@cldmv/fix-headers 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/index.cjs +39 -0
- package/index.mjs +7 -0
- package/package.json +56 -0
- package/src/cli.mjs +216 -0
- package/src/constants.mjs +15 -0
- package/src/core/file-discovery.mjs +133 -0
- package/src/core/fix-headers.mjs +210 -0
- package/src/detect/project.mjs +175 -0
- package/src/detectors/css.mjs +54 -0
- package/src/detectors/go.mjs +52 -0
- package/src/detectors/html.mjs +54 -0
- package/src/detectors/index.mjs +160 -0
- package/src/detectors/node.mjs +63 -0
- package/src/detectors/php.mjs +53 -0
- package/src/detectors/python.mjs +48 -0
- package/src/detectors/rust.mjs +49 -0
- package/src/detectors/shared.mjs +32 -0
- package/src/fix-header.mjs +30 -0
- package/src/header/parser.mjs +76 -0
- package/src/header/syntax.mjs +54 -0
- package/src/header/template.mjs +44 -0
- package/src/utils/fs.mjs +125 -0
- package/src/utils/git.mjs +97 -0
- package/src/utils/time.mjs +31 -0
- package/types/index.d.mts +2 -0
- package/types/src/cli.d.mts +44 -0
- package/types/src/constants.d.mts +7 -0
- package/types/src/core/file-discovery.d.mts +22 -0
- package/types/src/core/fix-headers.d.mts +86 -0
- package/types/src/detect/project.d.mts +81 -0
- package/types/src/detectors/css.d.mts +25 -0
- package/types/src/detectors/go.d.mts +25 -0
- package/types/src/detectors/html.d.mts +25 -0
- package/types/src/detectors/index.d.mts +66 -0
- package/types/src/detectors/node.d.mts +25 -0
- package/types/src/detectors/php.d.mts +25 -0
- package/types/src/detectors/python.d.mts +23 -0
- package/types/src/detectors/rust.d.mts +25 -0
- package/types/src/detectors/shared.d.mts +14 -0
- package/types/src/fix-header.d.mts +12 -0
- package/types/src/header/parser.d.mts +43 -0
- package/types/src/header/syntax.d.mts +44 -0
- package/types/src/header/template.d.mts +52 -0
- package/types/src/utils/fs.d.mts +50 -0
- package/types/src/utils/git.d.mts +40 -0
- package/types/src/utils/time.d.mts +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# fix-headers
|
|
2
|
+
|
|
3
|
+
Multi-language source header normalizer for Node.js projects.
|
|
4
|
+
|
|
5
|
+
`fix-headers` scans project files, auto-detects project metadata (language, root, project name, git author/email), and inserts or updates standard file headers.
|
|
6
|
+
|
|
7
|
+
[![npm version]][npm_version_url] [![npm downloads]][npm_downloads_url] <!-- [![GitHub release]][github_release_url] -->[![GitHub downloads]][github_downloads_url] [![Last commit]][last_commit_url] <!-- [![Release date]][release_date_url] -->[![npm last update]][npm_last_update_url] [![Coverage]][coverage_url]
|
|
8
|
+
|
|
9
|
+
[![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url]
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Auto-detects project type by marker files (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `composer.json`)
|
|
14
|
+
- Auto-detects author and email from git config/commit history
|
|
15
|
+
- Supports per-run overrides for every detected value
|
|
16
|
+
- Supports folder inclusion and exclusion configuration
|
|
17
|
+
- Supports detector-based monorepo scanning with nearest config resolution per file
|
|
18
|
+
- Supports per-detector syntax overrides for line and block comment tokens
|
|
19
|
+
- Supports both ESM and CJS consumers
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm i fix-headers
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### ESM
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import fixHeaders from "fix-headers";
|
|
33
|
+
|
|
34
|
+
const result = await fixHeaders({ dryRun: true });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## CLI
|
|
38
|
+
|
|
39
|
+
After install, use the package binary:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
fix-headers --dry-run --include-folder src --exclude-folder dist
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Local development usage:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run cli -- --dry-run --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Common CLI options:
|
|
52
|
+
|
|
53
|
+
- `--dry-run`
|
|
54
|
+
- `--json`
|
|
55
|
+
- `--cwd <path>`
|
|
56
|
+
- `--input <path>`
|
|
57
|
+
- `--include-folder <path>` (repeatable)
|
|
58
|
+
- `--exclude-folder <path>` (repeatable)
|
|
59
|
+
- `--include-extension <ext>` (repeatable)
|
|
60
|
+
- `--enable-detector <id>` / `--disable-detector <id>` (repeatable)
|
|
61
|
+
- `--project-name <name>`
|
|
62
|
+
- `--author-name <name>` / `--author-email <email>`
|
|
63
|
+
- `--company-name <name>`
|
|
64
|
+
- `--copyright-start-year <year>`
|
|
65
|
+
- `--config <json-file>`
|
|
66
|
+
|
|
67
|
+
### CommonJS
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
const fixHeaders = require("fix-headers");
|
|
71
|
+
|
|
72
|
+
const result = await fixHeaders({ dryRun: true });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `fixHeaders(options?)`
|
|
78
|
+
|
|
79
|
+
Runs header normalization. Project/language/author/email metadata is auto-detected internally on each run.
|
|
80
|
+
|
|
81
|
+
Important options:
|
|
82
|
+
|
|
83
|
+
- `cwd?: string` - start directory for project detection
|
|
84
|
+
- `input?: string` - explicit single file or folder path to process
|
|
85
|
+
- `dryRun?: boolean` - compute changes without writing files
|
|
86
|
+
- `configFile?: string` - load JSON options from file (resolved from `cwd`)
|
|
87
|
+
- `includeExtensions?: string[]` - file extensions to process
|
|
88
|
+
- `enabledDetectors?: string[]` - detector ids to enable (defaults to all)
|
|
89
|
+
- `disabledDetectors?: string[]` - detector ids to disable
|
|
90
|
+
- `detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }>` - override detector comment syntax tokens
|
|
91
|
+
- `includeFolders?: string[]` - project-relative folders to scan
|
|
92
|
+
- `excludeFolders?: string[]` - folder names or relative paths to exclude
|
|
93
|
+
- `projectName?: string`
|
|
94
|
+
- `language?: string`
|
|
95
|
+
- `projectRoot?: string`
|
|
96
|
+
- `marker?: string | null`
|
|
97
|
+
- `authorName?: string`
|
|
98
|
+
- `authorEmail?: string`
|
|
99
|
+
- `companyName?: string` (default: `Catalyzed Motivation Inc.`)
|
|
100
|
+
- `copyrightStartYear?: number` (default: current year)
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
const result = await fixHeaders({
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
dryRun: false,
|
|
108
|
+
configFile: "fix-headers.config.json",
|
|
109
|
+
includeFolders: ["src", "scripts"],
|
|
110
|
+
excludeFolders: ["src/generated", "dist"],
|
|
111
|
+
detectorSyntaxOverrides: {
|
|
112
|
+
node: {
|
|
113
|
+
blockStart: "/*",
|
|
114
|
+
blockLinePrefix: " * ",
|
|
115
|
+
blockEnd: " */"
|
|
116
|
+
},
|
|
117
|
+
python: {
|
|
118
|
+
linePrefix: ";;"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
projectName: "@scope/my-package",
|
|
122
|
+
companyName: "Catalyzed Motivation Inc.",
|
|
123
|
+
copyrightStartYear: 2013
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Notes
|
|
128
|
+
|
|
129
|
+
- `excludeFolders` supports both folder-name and nested path matching.
|
|
130
|
+
- For monorepos, each file resolves metadata from the closest detector config in its parent tree.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
Apache-2.0
|
|
135
|
+
|
|
136
|
+
<!-- Badge definitions -->
|
|
137
|
+
<!-- [github release]: https://img.shields.io/github/v/release/CLDMV/fix-headers?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 -->
|
|
138
|
+
<!-- [github_release_url]: https://github.com/CLDMV/fix-headers/releases -->
|
|
139
|
+
<!-- [release date]: https://img.shields.io/github/release-date/CLDMV/fix-headers?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 -->
|
|
140
|
+
<!-- [release_date_url]: https://github.com/CLDMV/fix-headers/releases -->
|
|
141
|
+
|
|
142
|
+
[npm version]: https://img.shields.io/npm/v/%40cldmv%2Ffix-headers.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
|
|
143
|
+
[npm_version_url]: https://www.npmjs.com/package/@cldmv/fix-headers
|
|
144
|
+
[npm downloads]: https://img.shields.io/npm/dm/%40cldmv%2Ffix-headers.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
|
|
145
|
+
[npm_downloads_url]: https://www.npmjs.com/package/@cldmv/fix-headers
|
|
146
|
+
[github downloads]: https://img.shields.io/github/downloads/CLDMV/fix-headers/total?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
|
|
147
|
+
[github_downloads_url]: https://github.com/CLDMV/fix-headers/releases
|
|
148
|
+
[last commit]: https://img.shields.io/github/last-commit/CLDMV/fix-headers?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
|
|
149
|
+
[last_commit_url]: https://github.com/CLDMV/fix-headers/commits
|
|
150
|
+
[npm last update]: https://img.shields.io/npm/last-update/%40cldmv%2Ffix-headers?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
|
|
151
|
+
[npm_last_update_url]: https://www.npmjs.com/package/@cldmv/fix-headers
|
|
152
|
+
[coverage]: https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FCLDMV%2Ffix-headers%2Fbadges%2Fcoverage.json&style=for-the-badge&logo=vitest&logoColor=white
|
|
153
|
+
[coverage_url]: https://github.com/CLDMV/fix-headers/blob/badges/coverage.json
|
|
154
|
+
[contributors]: https://img.shields.io/github/contributors/CLDMV/fix-headers.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
|
|
155
|
+
[contributors_url]: https://github.com/CLDMV/fix-headers/graphs/contributors
|
|
156
|
+
[sponsor shinrai]: https://img.shields.io/github/sponsors/shinrai?style=for-the-badge&logo=githubsponsors&logoColor=white&labelColor=EA4AAA&label=Sponsor
|
|
157
|
+
[sponsor_url]: https://github.com/sponsors/shinrai
|
package/index.cjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @Project: @cldmv/fix-headers
|
|
3
|
+
* @Filename: /index.cjs
|
|
4
|
+
* @Date: 2026-03-01 13:29:45 -08:00 (1772400585)
|
|
5
|
+
* @Author: Nate Hyson <CLDMV>
|
|
6
|
+
* @Email: <Shinrai@users.noreply.github.com>
|
|
7
|
+
* -----
|
|
8
|
+
* @Last modified by: Nate Hyson <CLDMV> (Shinrai@users.noreply.github.com)
|
|
9
|
+
* @Last modified time: 2026-03-01 16:29:36 -08:00 (1772411376)
|
|
10
|
+
* -----
|
|
11
|
+
* @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @fileoverview CJS shim for consuming the ESM-based fix-headers module.
|
|
16
|
+
* @module fix-headers/cjs-shim
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
"use strict";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Loads the ESM module implementation.
|
|
23
|
+
* @returns {Promise<import("./index.mjs")>} Loaded ESM module.
|
|
24
|
+
*/
|
|
25
|
+
function loadEsmModule() {
|
|
26
|
+
return import("./index.mjs");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Runs header normalization with auto-detection and override options.
|
|
31
|
+
* @param {Record<string, unknown>} [options] - Runtime options.
|
|
32
|
+
* @returns {Promise<unknown>} Header update report.
|
|
33
|
+
*/
|
|
34
|
+
async function fixHeaders(options) {
|
|
35
|
+
const mod = await loadEsmModule();
|
|
36
|
+
return mod.default(options);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = fixHeaders;
|
package/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cldmv/fix-headers",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Multi-language project header normalizer with auto-detection and override support.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.cjs",
|
|
7
|
+
"module": "./index.mjs",
|
|
8
|
+
"types": "./types/index.d.mts",
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"index.cjs",
|
|
12
|
+
"src/",
|
|
13
|
+
"types/"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./types/index.d.mts",
|
|
18
|
+
"import": "./index.mjs",
|
|
19
|
+
"require": "./index.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"fix-headers": "./src/cli.mjs"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "vitest run --config .configs/vitest.config.mjs",
|
|
27
|
+
"test:watch": "vitest --config .configs/vitest.config.mjs",
|
|
28
|
+
"ci:coverage": "vitest run --coverage --reporter=dot --maxWorkers=1 --config .configs/vitest.ci.config.mjs",
|
|
29
|
+
"types:build": "tsc -p .configs/tsconfig.json --noCheck",
|
|
30
|
+
"types:check": "tsc -p .configs/tsconfig.json --noEmit",
|
|
31
|
+
"test:coverage": "vitest run --config .configs/vitest.config.mjs --coverage",
|
|
32
|
+
"cli": "node ./src/cli.mjs"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"headers",
|
|
36
|
+
"copyright",
|
|
37
|
+
"automation",
|
|
38
|
+
"node-module"
|
|
39
|
+
],
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"vitest": ">=1.0.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"license": "Apache-2.0",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^25.3.0",
|
|
52
|
+
"typescript": "^5.9.3",
|
|
53
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
54
|
+
"vitest": "^3.2.4"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import fixHeaders from "./fix-header.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @fileoverview CLI entry point for running fix-headers from terminal commands.
|
|
10
|
+
* @module fix-headers/cli
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const HELP_TEXT = `fix-headers CLI\n\nUsage:\n fix-headers [options]\n\nOptions:\n -h, --help Show help\n --dry-run Compute changes without writing files\n --json Print JSON output\n --cwd <path> Working directory for project detection\n --input <path> Single file or folder input\n --include-folder <path> Include folder (repeatable)\n --exclude-folder <path> Exclude folder name/path (repeatable)\n --include-extension <ext> Include extension (repeatable)\n --enable-detector <id> Enable only specific detector (repeatable)\n --disable-detector <id> Disable detector by id (repeatable)\n --project-name <name> Override project name\n --language <id> Override language id\n --project-root <path> Override project root\n --marker <name|null> Override marker filename\n --author-name <name> Override author name\n --author-email <email> Override author email\n --company-name <name> Override company name\n --copyright-start-year <year> Override copyright start year\n --config <path> Load JSON options file\n\nExamples:\n fix-headers --dry-run --include-folder src\n fix-headers --project-name @scope/pkg --company-name "Catalyzed Motivation Inc."\n`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts CLI flag token to camelCase key.
|
|
17
|
+
* @param {string} token - CLI token without leading dashes.
|
|
18
|
+
* @returns {string} CamelCase key.
|
|
19
|
+
*/
|
|
20
|
+
function toCamelCase(token) {
|
|
21
|
+
return token.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parses CLI arguments into fixHeaders options and control flags.
|
|
26
|
+
* @param {string[]} argv - Process argument vector without node/script items.
|
|
27
|
+
* @returns {{
|
|
28
|
+
* options: Record<string, unknown>,
|
|
29
|
+
* help: boolean,
|
|
30
|
+
* json: boolean
|
|
31
|
+
* }} Parsed CLI payload.
|
|
32
|
+
*/
|
|
33
|
+
export function parseCliArgs(argv) {
|
|
34
|
+
/** @type {Record<string, unknown>} */
|
|
35
|
+
const options = {};
|
|
36
|
+
const control = { help: false, json: false };
|
|
37
|
+
const multiMap = {
|
|
38
|
+
"include-folder": "includeFolders",
|
|
39
|
+
"exclude-folder": "excludeFolders",
|
|
40
|
+
"include-extension": "includeExtensions",
|
|
41
|
+
"enable-detector": "enabledDetectors",
|
|
42
|
+
"disable-detector": "disabledDetectors"
|
|
43
|
+
};
|
|
44
|
+
const scalarMap = {
|
|
45
|
+
cwd: "cwd",
|
|
46
|
+
input: "input",
|
|
47
|
+
"project-name": "projectName",
|
|
48
|
+
language: "language",
|
|
49
|
+
"project-root": "projectRoot",
|
|
50
|
+
marker: "marker",
|
|
51
|
+
"author-name": "authorName",
|
|
52
|
+
"author-email": "authorEmail",
|
|
53
|
+
"company-name": "companyName",
|
|
54
|
+
"copyright-start-year": "copyrightStartYear",
|
|
55
|
+
config: "config"
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
59
|
+
const arg = argv[index];
|
|
60
|
+
if (arg === "-h" || arg === "--help") {
|
|
61
|
+
control.help = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (arg === "--json") {
|
|
65
|
+
control.json = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (arg === "--dry-run") {
|
|
69
|
+
options.dryRun = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!arg.startsWith("--")) {
|
|
73
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const flag = arg.slice(2);
|
|
77
|
+
if (multiMap[flag]) {
|
|
78
|
+
const value = argv[index + 1];
|
|
79
|
+
if (!value || value.startsWith("--")) {
|
|
80
|
+
throw new Error(`Missing value for --${flag}`);
|
|
81
|
+
}
|
|
82
|
+
index += 1;
|
|
83
|
+
const key = multiMap[flag];
|
|
84
|
+
const list = Array.isArray(options[key]) ? options[key] : [];
|
|
85
|
+
options[key] = [...list, value];
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (scalarMap[flag]) {
|
|
90
|
+
const value = argv[index + 1];
|
|
91
|
+
if (!value || value.startsWith("--")) {
|
|
92
|
+
throw new Error(`Missing value for --${flag}`);
|
|
93
|
+
}
|
|
94
|
+
index += 1;
|
|
95
|
+
const key = scalarMap[flag];
|
|
96
|
+
if (key === "copyrightStartYear") {
|
|
97
|
+
options[key] = Number.parseInt(value, 10);
|
|
98
|
+
if (Number.isNaN(options[key])) {
|
|
99
|
+
throw new Error(`Invalid number for --${flag}: ${value}`);
|
|
100
|
+
}
|
|
101
|
+
} else if (key === "marker" && value === "null") {
|
|
102
|
+
options[key] = null;
|
|
103
|
+
} else {
|
|
104
|
+
options[key] = value;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const camelKey = toCamelCase(flag);
|
|
110
|
+
const value = argv[index + 1];
|
|
111
|
+
if (!value || value.startsWith("--")) {
|
|
112
|
+
throw new Error(`Unknown or malformed flag: --${flag}`);
|
|
113
|
+
}
|
|
114
|
+
index += 1;
|
|
115
|
+
options[camelKey] = value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
options,
|
|
120
|
+
help: control.help,
|
|
121
|
+
json: control.json
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Loads extra options from a JSON config file.
|
|
127
|
+
* @param {Record<string, unknown>} options - Current options object.
|
|
128
|
+
* @returns {Promise<Record<string, unknown>>} Merged options object.
|
|
129
|
+
*/
|
|
130
|
+
export async function applyConfigFile(options) {
|
|
131
|
+
if (typeof options.config !== "string" || options.config.trim().length === 0) {
|
|
132
|
+
return options;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const baseDir = typeof options.cwd === "string" && options.cwd.length > 0 ? options.cwd : process.cwd();
|
|
136
|
+
const configPath = isAbsolute(options.config) ? options.config : join(baseDir, options.config);
|
|
137
|
+
const raw = await readFile(configPath, "utf8");
|
|
138
|
+
const parsed = JSON.parse(raw);
|
|
139
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
140
|
+
throw new Error("Config file must contain a JSON object");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const merged = { ...parsed, ...options };
|
|
144
|
+
delete merged.config;
|
|
145
|
+
/** @type {Record<string, unknown>} */
|
|
146
|
+
const output = merged;
|
|
147
|
+
return output;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Executes CLI flow and returns process-like exit code.
|
|
152
|
+
* @param {string[]} argv - CLI arguments.
|
|
153
|
+
* @param {{
|
|
154
|
+
* runner?: (options: Record<string, unknown>) => Promise<unknown>,
|
|
155
|
+
* stdout?: (message: string) => void,
|
|
156
|
+
* stderr?: (message: string) => void
|
|
157
|
+
* }} [deps={}] - Dependency overrides for tests.
|
|
158
|
+
* @returns {Promise<number>} Exit code.
|
|
159
|
+
*/
|
|
160
|
+
export async function runCli(argv, deps = {}) {
|
|
161
|
+
const runner = deps.runner || fixHeaders;
|
|
162
|
+
const stdout = deps.stdout || console.log;
|
|
163
|
+
const stderr = deps.stderr || console.error;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const parsed = parseCliArgs(argv);
|
|
167
|
+
if (parsed.help) {
|
|
168
|
+
stdout(HELP_TEXT);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const finalOptions = await applyConfigFile(parsed.options);
|
|
173
|
+
const result = await runner(finalOptions);
|
|
174
|
+
|
|
175
|
+
if (parsed.json) {
|
|
176
|
+
stdout(JSON.stringify(result, null, 2));
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (result && typeof result === "object") {
|
|
181
|
+
const report = /** @type {{filesScanned?: number, filesUpdated?: number, dryRun?: boolean}} */ (result);
|
|
182
|
+
stdout(
|
|
183
|
+
`fix-headers complete: scanned=${report.filesScanned ?? 0}, updated=${report.filesUpdated ?? 0}, dryRun=${report.dryRun === true}`
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
stdout("fix-headers complete");
|
|
187
|
+
}
|
|
188
|
+
return 0;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
191
|
+
stderr(`fix-headers failed: ${message}`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Executes CLI flow when the module is the process entrypoint.
|
|
198
|
+
* @param {string[]} [argv=process.argv] - Process argument vector.
|
|
199
|
+
* @param {string} [moduleUrl=import.meta.url] - Current module URL.
|
|
200
|
+
* @param {(args: string[]) => Promise<number>} [executor=runCli] - CLI executor.
|
|
201
|
+
* @returns {boolean} Whether the entrypoint branch was executed.
|
|
202
|
+
*/
|
|
203
|
+
export function runCliAsMain(argv = process.argv, moduleUrl = import.meta.url, executor = runCli) {
|
|
204
|
+
const isMain = argv[1] && moduleUrl === pathToFileURL(argv[1]).href;
|
|
205
|
+
if (!isMain) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
executor(argv.slice(2)).then((code) => {
|
|
210
|
+
process.exitCode = code;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
runCliAsMain();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared constants for project/language detection and header defaults.
|
|
3
|
+
* @module fix-headers/constants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { DETECTOR_PROFILES, getAllowedExtensions, getEnabledDetectors } from "./detectors/index.mjs";
|
|
7
|
+
|
|
8
|
+
/** @type {string} */
|
|
9
|
+
export const DEFAULT_COMPANY_NAME = "Catalyzed Motivation Inc.";
|
|
10
|
+
|
|
11
|
+
/** @type {number} */
|
|
12
|
+
export const DEFAULT_MAX_HEADER_SCAN_LINES = 40;
|
|
13
|
+
|
|
14
|
+
/** @type {Set<string>} */
|
|
15
|
+
export const DEFAULT_IGNORE_FOLDERS = new Set([".git", "node_modules", "dist", "build", "coverage", "tmp", ".next", ".turbo"]);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { join, relative, resolve } from "node:path";
|
|
2
|
+
import { DEFAULT_IGNORE_FOLDERS } from "../constants.mjs";
|
|
3
|
+
import { getAllowedExtensions } from "../detectors/index.mjs";
|
|
4
|
+
import { walkFiles } from "../utils/fs.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @fileoverview Resolves candidate source files for header updates based on language and options.
|
|
8
|
+
* @module fix-headers/core/file-discovery
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolves extension set from enabled detectors and optional override.
|
|
13
|
+
* @param {{
|
|
14
|
+
* includeExtensions?: string[],
|
|
15
|
+
* enabledDetectors?: string[],
|
|
16
|
+
* disabledDetectors?: string[]
|
|
17
|
+
* }} options - Extension options.
|
|
18
|
+
* @returns {Set<string>} Effective extension set.
|
|
19
|
+
*/
|
|
20
|
+
function resolveExtensions(options) {
|
|
21
|
+
return getAllowedExtensions(options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_IGNORED_FOLDERS = new Set(DEFAULT_IGNORE_FOLDERS);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalizes a user-provided folder path to a project-relative value.
|
|
28
|
+
* @param {string} folderPath - Folder path string.
|
|
29
|
+
* @returns {string} Normalized path.
|
|
30
|
+
*/
|
|
31
|
+
function normalizeFolderPath(folderPath) {
|
|
32
|
+
return folderPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Determines include roots from explicit include folders.
|
|
37
|
+
* @param {string[] | undefined} includeFolders - Include folder option.
|
|
38
|
+
* @returns {string[]} Effective include roots.
|
|
39
|
+
*/
|
|
40
|
+
function resolveIncludeFolders(includeFolders) {
|
|
41
|
+
if (Array.isArray(includeFolders) && includeFolders.length > 0) {
|
|
42
|
+
return includeFolders;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Builds a directory exclusion matcher from path and folder-name lists.
|
|
50
|
+
* @param {string} projectRoot - Project root path.
|
|
51
|
+
* @param {string[] | undefined} excludeFolders - Folder exclusions.
|
|
52
|
+
* @returns {(targetPath: string, targetName: string) => boolean} Exclusion predicate.
|
|
53
|
+
*/
|
|
54
|
+
function buildExclusionMatcher(projectRoot, excludeFolders) {
|
|
55
|
+
if (!Array.isArray(excludeFolders) || excludeFolders.length === 0) {
|
|
56
|
+
return () => false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const absoluteRoot = resolve(projectRoot);
|
|
60
|
+
const pathExclusions = [];
|
|
61
|
+
const nameExclusions = new Set();
|
|
62
|
+
|
|
63
|
+
for (const entry of excludeFolders) {
|
|
64
|
+
if (typeof entry !== "string") {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const normalized = normalizeFolderPath(entry);
|
|
69
|
+
if (normalized.length === 0) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (normalized.includes("/")) {
|
|
74
|
+
pathExclusions.push(resolve(absoluteRoot, normalized));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
nameExclusions.add(normalized);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (targetPath, targetName) => {
|
|
82
|
+
if (nameExclusions.has(targetName)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const excludedPath of pathExclusions) {
|
|
87
|
+
if (targetPath === excludedPath || targetPath.startsWith(`${excludedPath}/`)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const relPath = normalizeFolderPath(relative(absoluteRoot, targetPath));
|
|
93
|
+
return pathExclusions.some((excludedPath) => {
|
|
94
|
+
const relExcluded = normalizeFolderPath(relative(absoluteRoot, excludedPath));
|
|
95
|
+
return relPath === relExcluded || relPath.startsWith(`${relExcluded}/`);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Discovers source files for processing.
|
|
102
|
+
* @param {{
|
|
103
|
+
* projectRoot: string,
|
|
104
|
+
* language?: string,
|
|
105
|
+
* includeExtensions?: string[],
|
|
106
|
+
* enabledDetectors?: string[],
|
|
107
|
+
* disabledDetectors?: string[],
|
|
108
|
+
* includeFolders?: string[],
|
|
109
|
+
* excludeFolders?: string[]
|
|
110
|
+
* }} options - File discovery options.
|
|
111
|
+
* @returns {Promise<string[]>} Absolute file paths.
|
|
112
|
+
*/
|
|
113
|
+
export async function discoverFiles(options) {
|
|
114
|
+
const allowedExtensions = resolveExtensions(options);
|
|
115
|
+
const includeFolders = resolveIncludeFolders(options.includeFolders);
|
|
116
|
+
const excludeFolders = Array.isArray(options.excludeFolders) ? options.excludeFolders : [];
|
|
117
|
+
const exclusionMatcher = buildExclusionMatcher(options.projectRoot, excludeFolders);
|
|
118
|
+
|
|
119
|
+
const roots =
|
|
120
|
+
includeFolders.length > 0 ? includeFolders.map((relativePath) => join(options.projectRoot, relativePath)) : [options.projectRoot];
|
|
121
|
+
|
|
122
|
+
const files = [];
|
|
123
|
+
for (const rootPath of roots) {
|
|
124
|
+
const discovered = await walkFiles(rootPath, {
|
|
125
|
+
allowedExtensions,
|
|
126
|
+
ignoreFolders: DEFAULT_IGNORED_FOLDERS,
|
|
127
|
+
shouldSkipDirectory: exclusionMatcher
|
|
128
|
+
});
|
|
129
|
+
files.push(...discovered);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return files.filter((filePath) => !exclusionMatcher(filePath, filePath.split("/").at(-1) || ""));
|
|
133
|
+
}
|