@cldmv/fix-headers 1.0.0 → 1.1.1

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 CHANGED
@@ -10,7 +10,7 @@ Multi-language source header normalizer for Node.js projects.
10
10
 
11
11
  ## Features
12
12
 
13
- - Auto-detects project type by marker files (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `composer.json`)
13
+ - Auto-detects project type by marker files (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `composer.json`) and YAML files (`.yaml`, `.yml`)
14
14
  - Auto-detects author and email from git config/commit history
15
15
  - Supports per-run overrides for every detected value
16
16
  - Supports folder inclusion and exclusion configuration
@@ -52,6 +52,9 @@ Common CLI options:
52
52
 
53
53
  - `--dry-run`
54
54
  - `--json`
55
+ - `--sample-output`
56
+ - `--force-author-update`
57
+ - `--use-gpg-signer-author`
55
58
  - `--cwd <path>`
56
59
  - `--input <path>`
57
60
  - `--include-folder <path>` (repeatable)
@@ -83,11 +86,12 @@ Important options:
83
86
  - `cwd?: string` - start directory for project detection
84
87
  - `input?: string` - explicit single file or folder path to process
85
88
  - `dryRun?: boolean` - compute changes without writing files
89
+ - `sampleOutput?: boolean` - include previous/new header sample text for changed files
86
90
  - `configFile?: string` - load JSON options from file (resolved from `cwd`)
87
91
  - `includeExtensions?: string[]` - file extensions to process
88
92
  - `enabledDetectors?: string[]` - detector ids to enable (defaults to all)
89
93
  - `disabledDetectors?: string[]` - detector ids to disable
90
- - `detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }>` - override detector comment syntax tokens
94
+ - `detectorSyntaxOverrides?: Record<string, { linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }>` - override detector comment syntax tokens
91
95
  - `includeFolders?: string[]` - project-relative folders to scan
92
96
  - `excludeFolders?: string[]` - folder names or relative paths to exclude
93
97
  - `projectName?: string`
@@ -96,6 +100,8 @@ Important options:
96
100
  - `marker?: string | null`
97
101
  - `authorName?: string`
98
102
  - `authorEmail?: string`
103
+ - `forceAuthorUpdate?: boolean` - force update `@Author`/`@Email` to detected or overridden current values
104
+ - `useGpgSignerAuthor?: boolean` - use signed-commit UID (`%GS`) for detected `@Author` (includes signer comment when present)
99
105
  - `companyName?: string` (default: `Catalyzed Motivation Inc.`)
100
106
  - `copyrightStartYear?: number` (default: current year)
101
107
 
@@ -115,7 +121,8 @@ const result = await fixHeaders({
115
121
  blockEnd: " */"
116
122
  },
117
123
  python: {
118
- linePrefix: ";;"
124
+ linePrefix: ";;",
125
+ lineSeparator: " "
119
126
  }
120
127
  },
121
128
  projectName: "@scope/my-package",
@@ -128,6 +135,7 @@ const result = await fixHeaders({
128
135
 
129
136
  - `excludeFolders` supports both folder-name and nested path matching.
130
137
  - For monorepos, each file resolves metadata from the closest detector config in its parent tree.
138
+ - With `sampleOutput` enabled, each changed file includes `previousValue`, `newValue`, and `detectedValues` in results.
131
139
 
132
140
  ## License
133
141
 
package/index.cjs CHANGED
@@ -2,13 +2,13 @@
2
2
  * @Project: @cldmv/fix-headers
3
3
  * @Filename: /index.cjs
4
4
  * @Date: 2026-03-01 13:29:45 -08:00 (1772400585)
5
- * @Author: Nate Hyson <CLDMV>
5
+ * @Author: Nate Corcoran <CLDMV>
6
6
  * @Email: <Shinrai@users.noreply.github.com>
7
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)
8
+ * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2026-03-01T17:59:32-08:00 (1772416772)
10
10
  * -----
11
- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
11
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
12
12
  */
13
13
 
14
14
  /**
package/index.mjs CHANGED
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /index.mjs
4
+ * @Date: 2026-03-01T17:59:32-08:00 (1772416772)
5
+ * @Author: Nate Corcoran <CLDMV>
6
+ * @Email: <Shinrai@users.noreply.github.com>
7
+ * -----
8
+ * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2026-03-01T17:59:32-08:00 (1772416772)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
1
14
  /**
2
15
  * @fileoverview ESM shim for the fix-headers package API.
3
16
  * @module fix-headers/esm-shim
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cldmv/fix-headers",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Multi-language project header normalizer with auto-detection and override support.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",
package/src/cli.mjs CHANGED
@@ -1,8 +1,21 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * @Project: @cldmv/fix-headers
4
+ * @Filename: /src/cli.mjs
5
+ * @Date: 2026-03-01T17:59:32-08:00 (1772416772)
6
+ * @Author: Nate Corcoran <CLDMV>
7
+ * @Email: <Shinrai@users.noreply.github.com>
8
+ * -----
9
+ * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
10
+ * @Last modified time: 2026-03-01T17:59:32-08:00 (1772416772)
11
+ * -----
12
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
13
+ */
2
14
 
3
15
  import { readFile } from "node:fs/promises";
16
+ import { realpathSync } from "node:fs";
4
17
  import { isAbsolute, join } from "node:path";
5
- import { pathToFileURL } from "node:url";
18
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
19
  import fixHeaders from "./fix-header.mjs";
7
20
 
8
21
  /**
@@ -10,7 +23,7 @@ import fixHeaders from "./fix-header.mjs";
10
23
  * @module fix-headers/cli
11
24
  */
12
25
 
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`;
26
+ 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 --verbose Print updated file paths in summary mode\n --sample-output Show previous/new header sample for changed files\n --force-author-update Always update @Author/@Email to detected/current values\n --use-gpg-signer-author Use signed-commit UID (%GS) for detected @Author\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
27
 
15
28
  /**
16
29
  * Converts CLI flag token to camelCase key.
@@ -65,10 +78,26 @@ export function parseCliArgs(argv) {
65
78
  control.json = true;
66
79
  continue;
67
80
  }
81
+ if (arg === "--verbose") {
82
+ options.verbose = true;
83
+ continue;
84
+ }
68
85
  if (arg === "--dry-run") {
69
86
  options.dryRun = true;
70
87
  continue;
71
88
  }
89
+ if (arg === "--sample-output") {
90
+ options.sampleOutput = true;
91
+ continue;
92
+ }
93
+ if (arg === "--force-author-update") {
94
+ options.forceAuthorUpdate = true;
95
+ continue;
96
+ }
97
+ if (arg === "--use-gpg-signer-author") {
98
+ options.useGpgSignerAuthor = true;
99
+ continue;
100
+ }
72
101
  if (!arg.startsWith("--")) {
73
102
  throw new Error(`Unexpected argument: ${arg}`);
74
103
  }
@@ -82,7 +111,7 @@ export function parseCliArgs(argv) {
82
111
  index += 1;
83
112
  const key = multiMap[flag];
84
113
  const list = Array.isArray(options[key]) ? options[key] : [];
85
- options[key] = [...list, value];
114
+ options[key] = Array.from(new Set([...list, value]));
86
115
  continue;
87
116
  }
88
117
 
@@ -147,6 +176,58 @@ export async function applyConfigFile(options) {
147
176
  return output;
148
177
  }
149
178
 
179
+ /**
180
+ * Prints per-file previous/new header samples when available.
181
+ * @param {(message: string) => void} stdout - Standard output writer.
182
+ * @param {unknown} result - Runner result object.
183
+ * @returns {void}
184
+ */
185
+ function printSampleOutput(stdout, result) {
186
+ if (!result || typeof result !== "object") {
187
+ return;
188
+ }
189
+
190
+ const report =
191
+ /** @type {{changes?: Array<{file?: string, changed?: boolean, sample?: { previousValue?: string | null, newValue?: string, detectedValues?: Record<string, unknown> }}>}} */ (
192
+ result
193
+ );
194
+ const changes = Array.isArray(report.changes) ? report.changes : [];
195
+ for (const change of changes) {
196
+ if (!change || change.changed !== true || !change.sample || typeof change.sample.newValue !== "string") {
197
+ continue;
198
+ }
199
+
200
+ stdout(`sample: ${change.file || "<unknown-file>"}`);
201
+ stdout("previous:");
202
+ stdout(change.sample.previousValue === null ? "(none)" : String(change.sample.previousValue));
203
+ stdout("new:");
204
+ stdout(change.sample.newValue);
205
+ if (change.sample.detectedValues && typeof change.sample.detectedValues === "object") {
206
+ stdout("detected-values:");
207
+ stdout(JSON.stringify(change.sample.detectedValues, null, 2));
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Prints changed file paths from result payload.
214
+ * @param {(message: string) => void} stdout - Standard output writer.
215
+ * @param {unknown} result - Runner result object.
216
+ * @returns {void}
217
+ */
218
+ function printChangedFiles(stdout, result) {
219
+ if (!result || typeof result !== "object") {
220
+ return;
221
+ }
222
+
223
+ const report = /** @type {{changes?: Array<{file?: string, changed?: boolean}>}} */ (result);
224
+ const changes = Array.isArray(report.changes) ? report.changes : [];
225
+ const updatedFiles = changes.filter((change) => change && change.changed === true).map((change) => change.file || "<unknown-file>");
226
+ for (const file of updatedFiles) {
227
+ stdout(`updated: ${file}`);
228
+ }
229
+ }
230
+
150
231
  /**
151
232
  * Executes CLI flow and returns process-like exit code.
152
233
  * @param {string[]} argv - CLI arguments.
@@ -182,7 +263,19 @@ export async function runCli(argv, deps = {}) {
182
263
  stdout(
183
264
  `fix-headers complete: scanned=${report.filesScanned ?? 0}, updated=${report.filesUpdated ?? 0}, dryRun=${report.dryRun === true}`
184
265
  );
266
+ if (finalOptions.verbose === true) {
267
+ printChangedFiles(stdout, result);
268
+ }
269
+ if (finalOptions.sampleOutput === true) {
270
+ printSampleOutput(stdout, result);
271
+ }
185
272
  } else {
273
+ if (finalOptions.verbose === true) {
274
+ printChangedFiles(stdout, result);
275
+ }
276
+ if (finalOptions.sampleOutput === true) {
277
+ printSampleOutput(stdout, result);
278
+ }
186
279
  stdout("fix-headers complete");
187
280
  }
188
281
  return 0;
@@ -201,7 +294,16 @@ export async function runCli(argv, deps = {}) {
201
294
  * @returns {boolean} Whether the entrypoint branch was executed.
202
295
  */
203
296
  export function runCliAsMain(argv = process.argv, moduleUrl = import.meta.url, executor = runCli) {
204
- const isMain = argv[1] && moduleUrl === pathToFileURL(argv[1]).href;
297
+ let isMain = false;
298
+ if (argv[1]) {
299
+ try {
300
+ const argvRealPath = realpathSync(argv[1]);
301
+ const moduleRealPath = realpathSync(fileURLToPath(moduleUrl));
302
+ isMain = argvRealPath === moduleRealPath;
303
+ } catch {
304
+ isMain = moduleUrl === pathToFileURL(argv[1]).href;
305
+ }
306
+ }
205
307
  if (!isMain) {
206
308
  return false;
207
309
  }
package/src/constants.mjs CHANGED
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/constants.mjs
4
+ * @Date: 2026-03-01T17:59:32-08:00 (1772416772)
5
+ * @Author: Nate Corcoran <CLDMV>
6
+ * @Email: <Shinrai@users.noreply.github.com>
7
+ * -----
8
+ * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2026-03-01T17:59:32-08:00 (1772416772)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
1
14
  /**
2
15
  * @fileoverview Shared constants for project/language detection and header defaults.
3
16
  * @module fix-headers/constants
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/core/file-discovery.mjs
4
+ * @Date: 2026-03-01T17:59:32-08:00 (1772416772)
5
+ * @Author: Nate Corcoran <CLDMV>
6
+ * @Email: <Shinrai@users.noreply.github.com>
7
+ * -----
8
+ * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2026-03-01T17:59:32-08:00 (1772416772)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
1
14
  import { join, relative, resolve } from "node:path";
2
15
  import { DEFAULT_IGNORE_FOLDERS } from "../constants.mjs";
3
16
  import { getAllowedExtensions } from "../detectors/index.mjs";
@@ -1,9 +1,22 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/core/fix-headers.mjs
4
+ * @Date: 2026-03-01T17:59:32-08:00 (1772416772)
5
+ * @Author: Nate Corcoran <CLDMV>
6
+ * @Email: <Shinrai@users.noreply.github.com>
7
+ * -----
8
+ * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2026-03-01T17:59:32-08:00 (1772416772)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
1
14
  import { readFile, stat, writeFile } from "node:fs/promises";
2
15
  import { relative, resolve } from "node:path";
3
16
  import { discoverFiles } from "./file-discovery.mjs";
4
17
  import { resolveProjectMetadata } from "../detect/project.mjs";
5
18
  import { buildHeader } from "../header/template.mjs";
6
- import { replaceOrInsertHeader } from "../header/parser.mjs";
19
+ import { findProjectHeader, replaceOrInsertHeader } from "../header/parser.mjs";
7
20
  import { readFileDates } from "../utils/fs.mjs";
8
21
  import { getGitCreationDate, getGitLastModifiedDate } from "../utils/git.mjs";
9
22
  import { toDatePayload } from "../utils/time.mjs";
@@ -14,9 +27,12 @@ import { toDatePayload } from "../utils/time.mjs";
14
27
  * input?: string,
15
28
  * dryRun?: boolean,
16
29
  * configFile?: string,
30
+ * sampleOutput?: boolean,
31
+ * forceAuthorUpdate?: boolean,
32
+ * useGpgSignerAuthor?: boolean,
17
33
  * enabledDetectors?: string[],
18
34
  * disabledDetectors?: string[],
19
- * detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }>,
35
+ * detectorSyntaxOverrides?: Record<string, { linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }>,
20
36
  * includeFolders?: string[],
21
37
  * excludeFolders?: string[],
22
38
  * includeExtensions?: string[],
@@ -47,7 +63,20 @@ import { toDatePayload } from "../utils/time.mjs";
47
63
  * filesScanned: number,
48
64
  * filesUpdated: number,
49
65
  * dryRun: boolean,
50
- * changes: Array<{file: string, changed: boolean}>
66
+ * changes: Array<{file: string, changed: boolean, sample?: { previousValue: string | null, newValue: string, detectedValues?: {
67
+ * projectName: string,
68
+ * language: string,
69
+ * projectRoot: string,
70
+ * marker: string | null,
71
+ * authorName: string,
72
+ * authorEmail: string,
73
+ * companyName: string,
74
+ * copyrightStartYear: number,
75
+ * createdAtSource: string,
76
+ * lastModifiedAtSource: string,
77
+ * createdAt: {date: string, timestamp: number},
78
+ * lastModifiedAt: {date: string, timestamp: number}
79
+ * } }}>
51
80
  * }} FixHeadersResult
52
81
  */
53
82
 
@@ -83,6 +112,65 @@ async function resolveRuntimeOptions(options) {
83
112
  return output;
84
113
  }
85
114
 
115
+ /**
116
+ * Extracts original author identity from an existing header block.
117
+ * @param {string} headerText - Existing header content.
118
+ * @returns {{ authorName?: string, authorEmail?: string }} Parsed identity values.
119
+ */
120
+ function extractHeaderAuthorIdentity(headerText) {
121
+ const authorMatch = headerText.match(/@Author:\s*(.+)$/m);
122
+ const emailMatch = headerText.match(/@Email:\s*<([^>\n]+)>/m);
123
+
124
+ return {
125
+ authorName: authorMatch?.[1]?.trim(),
126
+ authorEmail: emailMatch?.[1]?.trim()
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Extracts original created-at payload from an existing header block.
132
+ * @param {string} headerText - Existing header content.
133
+ * @returns {{date: string, timestamp: number} | null} Parsed created-at value.
134
+ */
135
+ function extractHeaderCreatedAt(headerText) {
136
+ const dateMatch = headerText.match(/@Date:\s*(.+?)\s*\((\d+)\)$/m);
137
+ if (!dateMatch || !dateMatch[1] || !dateMatch[2]) {
138
+ return null;
139
+ }
140
+
141
+ const timestamp = Number.parseInt(dateMatch[2], 10);
142
+ if (Number.isNaN(timestamp)) {
143
+ return null;
144
+ }
145
+
146
+ return {
147
+ date: dateMatch[1].trim(),
148
+ timestamp
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Extracts original last-modified payload from an existing header block.
154
+ * @param {string} headerText - Existing header content.
155
+ * @returns {{date: string, timestamp: number} | null} Parsed last-modified value.
156
+ */
157
+ function extractHeaderLastModifiedAt(headerText) {
158
+ const modifiedMatch = headerText.match(/@Last modified time:\s*(.+?)\s*\((\d+)\)$/m);
159
+ if (!modifiedMatch || !modifiedMatch[1] || !modifiedMatch[2]) {
160
+ return null;
161
+ }
162
+
163
+ const timestamp = Number.parseInt(modifiedMatch[2], 10);
164
+ if (Number.isNaN(timestamp)) {
165
+ return null;
166
+ }
167
+
168
+ return {
169
+ date: modifiedMatch[1].trim(),
170
+ timestamp
171
+ };
172
+ }
173
+
86
174
  /**
87
175
  * @fileoverview Main header-fixing engine with auto-detection and override support.
88
176
  * @module fix-headers/core/fix-headers
@@ -138,6 +226,7 @@ export async function fixHeaders(options = {}) {
138
226
  }
139
227
 
140
228
  const currentYear = new Date().getFullYear();
229
+ /** @type {FixHeadersResult["changes"]} */
141
230
  const changes = [];
142
231
  const detectedProjects = new Set();
143
232
  let filesUpdated = 0;
@@ -152,16 +241,33 @@ export async function fixHeaders(options = {}) {
152
241
  detectedProjects.add(`${fileMetadata.language}:${fileMetadata.projectRoot}`);
153
242
  const relativePath = relative(scanRoot, filePath);
154
243
  const original = await readFile(filePath, "utf8");
244
+ const existingHeader = findProjectHeader(original, filePath, {
245
+ language: fileMetadata.language,
246
+ enabledDetectors: effectiveOptions.enabledDetectors,
247
+ disabledDetectors: effectiveOptions.disabledDetectors,
248
+ detectorSyntaxOverrides: effectiveOptions.detectorSyntaxOverrides
249
+ });
250
+ const existingHeaderText = existingHeader ? original.slice(existingHeader.start, existingHeader.end) : "";
251
+ const existingIdentity = existingHeaderText.length > 0 ? extractHeaderAuthorIdentity(existingHeaderText) : {};
252
+ const existingCreatedAt = existingHeaderText.length > 0 ? extractHeaderCreatedAt(existingHeaderText) : null;
253
+ const existingLastModifiedAt = existingHeaderText.length > 0 ? extractHeaderLastModifiedAt(existingHeaderText) : null;
155
254
  const filesystemDates = await readFileDates(filePath);
156
255
 
157
256
  const metadataRelativePath = relative(fileMetadata.projectRoot, filePath);
158
257
  const gitCreated = await getGitCreationDate(fileMetadata.projectRoot, metadataRelativePath);
159
258
  const gitLastUpdated = await getGitLastModifiedDate(fileMetadata.projectRoot, metadataRelativePath);
160
259
 
161
- const createdAt = gitCreated || toDatePayload(filesystemDates.createdAt);
162
- const lastModifiedAt = gitLastUpdated || toDatePayload(filesystemDates.updatedAt);
260
+ const createdAtSource = existingCreatedAt ? "existing-header" : gitCreated ? "git-created" : "filesystem-created";
261
+ const comparisonLastModifiedAtSource = existingLastModifiedAt
262
+ ? "existing-header"
263
+ : gitLastUpdated
264
+ ? "git-last-modified"
265
+ : "filesystem-updated";
266
+ const createdAt = existingCreatedAt || gitCreated || toDatePayload(filesystemDates.createdAt);
267
+ const comparisonLastModifiedAt = existingLastModifiedAt || gitLastUpdated || toDatePayload(filesystemDates.updatedAt);
268
+ const shouldForceAuthorUpdate = effectiveOptions.forceAuthorUpdate === true;
163
269
 
164
- const header = buildHeader({
270
+ const comparisonHeader = buildHeader({
165
271
  absoluteFilePath: filePath,
166
272
  language: fileMetadata.language,
167
273
  syntaxOptions: {
@@ -172,22 +278,90 @@ export async function fixHeaders(options = {}) {
172
278
  },
173
279
  projectRoot: fileMetadata.projectRoot,
174
280
  projectName: fileMetadata.projectName,
281
+ createdByName: shouldForceAuthorUpdate ? fileMetadata.authorName : existingIdentity.authorName || fileMetadata.authorName,
282
+ createdByEmail: shouldForceAuthorUpdate ? fileMetadata.authorEmail : existingIdentity.authorEmail || fileMetadata.authorEmail,
283
+ lastModifiedByName: fileMetadata.authorName,
284
+ lastModifiedByEmail: fileMetadata.authorEmail,
175
285
  authorName: fileMetadata.authorName,
176
286
  authorEmail: fileMetadata.authorEmail,
177
287
  createdAt,
178
- lastModifiedAt,
288
+ lastModifiedAt: comparisonLastModifiedAt,
179
289
  copyrightStartYear: fileMetadata.copyrightStartYear,
180
290
  companyName: fileMetadata.companyName,
181
291
  currentYear
182
292
  });
183
293
 
184
- const replacement = replaceOrInsertHeader(original, header, filePath, {
294
+ const comparisonReplacement = replaceOrInsertHeader(original, comparisonHeader, filePath, {
185
295
  language: fileMetadata.language,
186
296
  enabledDetectors: effectiveOptions.enabledDetectors,
187
297
  disabledDetectors: effectiveOptions.disabledDetectors,
188
298
  detectorSyntaxOverrides: effectiveOptions.detectorSyntaxOverrides
189
299
  });
190
- changes.push({ file: relativePath, changed: replacement.changed });
300
+ const needsUpdate = comparisonReplacement.changed;
301
+ const finalLastModifiedAt = needsUpdate ? toDatePayload(new Date()) : comparisonLastModifiedAt;
302
+ const lastModifiedAtSource = needsUpdate ? "current-time-on-change" : comparisonLastModifiedAtSource;
303
+
304
+ const header = needsUpdate
305
+ ? buildHeader({
306
+ absoluteFilePath: filePath,
307
+ language: fileMetadata.language,
308
+ syntaxOptions: {
309
+ language: fileMetadata.language,
310
+ enabledDetectors: effectiveOptions.enabledDetectors,
311
+ disabledDetectors: effectiveOptions.disabledDetectors,
312
+ detectorSyntaxOverrides: effectiveOptions.detectorSyntaxOverrides
313
+ },
314
+ projectRoot: fileMetadata.projectRoot,
315
+ projectName: fileMetadata.projectName,
316
+ createdByName: shouldForceAuthorUpdate ? fileMetadata.authorName : existingIdentity.authorName || fileMetadata.authorName,
317
+ createdByEmail: shouldForceAuthorUpdate ? fileMetadata.authorEmail : existingIdentity.authorEmail || fileMetadata.authorEmail,
318
+ lastModifiedByName: fileMetadata.authorName,
319
+ lastModifiedByEmail: fileMetadata.authorEmail,
320
+ authorName: fileMetadata.authorName,
321
+ authorEmail: fileMetadata.authorEmail,
322
+ createdAt,
323
+ lastModifiedAt: finalLastModifiedAt,
324
+ copyrightStartYear: fileMetadata.copyrightStartYear,
325
+ companyName: fileMetadata.companyName,
326
+ currentYear
327
+ })
328
+ : comparisonHeader;
329
+
330
+ const replacement = needsUpdate
331
+ ? replaceOrInsertHeader(original, header, filePath, {
332
+ language: fileMetadata.language,
333
+ enabledDetectors: effectiveOptions.enabledDetectors,
334
+ disabledDetectors: effectiveOptions.disabledDetectors,
335
+ detectorSyntaxOverrides: effectiveOptions.detectorSyntaxOverrides
336
+ })
337
+ : comparisonReplacement;
338
+ const changeEntry = {
339
+ file: relativePath,
340
+ changed: replacement.changed
341
+ };
342
+
343
+ if (effectiveOptions.sampleOutput === true && replacement.changed) {
344
+ changeEntry.sample = {
345
+ previousValue: existingHeaderText.length > 0 ? existingHeaderText.trimEnd() : null,
346
+ newValue: header,
347
+ detectedValues: {
348
+ projectName: fileMetadata.projectName,
349
+ language: fileMetadata.language,
350
+ projectRoot: fileMetadata.projectRoot,
351
+ marker: fileMetadata.marker,
352
+ authorName: fileMetadata.authorName,
353
+ authorEmail: fileMetadata.authorEmail,
354
+ companyName: fileMetadata.companyName,
355
+ copyrightStartYear: fileMetadata.copyrightStartYear,
356
+ createdAtSource,
357
+ lastModifiedAtSource,
358
+ createdAt,
359
+ lastModifiedAt: finalLastModifiedAt
360
+ }
361
+ };
362
+ }
363
+
364
+ changes.push(changeEntry);
191
365
 
192
366
  if (!replacement.changed) {
193
367
  continue;
@@ -2,26 +2,13 @@
2
2
  * @Project: @cldmv/fix-headers
3
3
  * @Filename: /src/detect/project.mjs
4
4
  * @Date: 2026-03-01 13:32:57 -08:00 (1772400777)
5
- * @Author: Nate Hyson <CLDMV>
5
+ * @Author: Nate Corcoran <CLDMV>
6
6
  * @Email: <Shinrai@users.noreply.github.com>
7
7
  * -----
8
- * @Last modified by: Nate Hyson <CLDMV> (Shinrai@users.noreply.github.com)
9
- * @Last modified time: 2026-03-01 16:29:34 -08:00 (1772411374)
8
+ * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2026-03-01T17:59:32-08:00 (1772416772)
10
10
  * -----
11
- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
12
- */
13
-
14
- /**
15
- * @Project: @cldmv/fix-headers
16
- * @Filename: /src/detect/project.mjs
17
- * @Date: 2026-03-01 13:32:57 -08:00 (1772400777)
18
- * @Author: Nate Hyson <CLDMV>
19
- * @Email: <Shinrai@users.noreply.github.com>
20
- * -----
21
- * @Last modified by: Nate Hyson <CLDMV> (Shinrai@users.noreply.github.com)
22
- * @Last modified time: 2026-03-01 16:27:47 -08:00 (1772411267)
23
- * -----
24
- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
11
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
25
12
  */
26
13
 
27
14
  import { dirname, extname, join, resolve } from "node:path";
@@ -136,6 +123,7 @@ export async function detectProjectFromMarkers(cwd, options = {}) {
136
123
  * language?: string,
137
124
  * projectRoot?: string,
138
125
  * marker?: string | null,
126
+ * useGpgSignerAuthor?: boolean,
139
127
  * authorName?: string,
140
128
  * authorEmail?: string,
141
129
  * companyName?: string,
@@ -159,7 +147,9 @@ export async function resolveProjectMetadata(options = {}) {
159
147
  const detectFrom = options.targetFilePath ? dirname(cwd) : cwd;
160
148
  const preferredExtension = options.targetFilePath ? extname(cwd).toLowerCase() : "";
161
149
  const detected = await detectProjectFromMarkers(detectFrom, { detectors, preferredExtension });
162
- const gitAuthor = await detectGitAuthor(detected.rootDir);
150
+ const gitAuthor = await detectGitAuthor(detected.rootDir, {
151
+ useGpgSignerAuthor: options.useGpgSignerAuthor === true
152
+ });
163
153
  const currentYear = new Date().getFullYear();
164
154
 
165
155
  return {
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/detectors/css.mjs
4
+ * @Date: 2026-03-01T17:59:32-08:00 (1772416772)
5
+ * @Author: Nate Corcoran <CLDMV>
6
+ * @Email: <Shinrai@users.noreply.github.com>
7
+ * -----
8
+ * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2026-03-01T17:59:32-08:00 (1772416772)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
1
14
  import { extname } from "node:path";
2
15
  import { findNearestMarker } from "./shared.mjs";
3
16
 
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/detectors/go.mjs
4
+ * @Date: 2026-03-01T17:59:32-08:00 (1772416772)
5
+ * @Author: Nate Corcoran <CLDMV>
6
+ * @Email: <Shinrai@users.noreply.github.com>
7
+ * -----
8
+ * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2026-03-01T17:59:32-08:00 (1772416772)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
1
14
  import { extname } from "node:path";
2
15
  import { findNearestMarker } from "./shared.mjs";
3
16
 
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/detectors/html.mjs
4
+ * @Date: 2026-03-01T17:59:32-08:00 (1772416772)
5
+ * @Author: Nate Corcoran <CLDMV>
6
+ * @Email: <Shinrai@users.noreply.github.com>
7
+ * -----
8
+ * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2026-03-01T17:59:32-08:00 (1772416772)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
1
14
  import { extname } from "node:path";
2
15
  import { findNearestMarker } from "./shared.mjs";
3
16