@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.
@@ -2,13 +2,13 @@
2
2
  * @Project: @cldmv/fix-headers
3
3
  * @Filename: /src/detectors/index.mjs
4
4
  * @Date: 2026-03-01 16:34:41 -08:00 (1772411681)
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 17:57:59 -08:00 (1772416679)
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
  import { readdir } from "node:fs/promises";
@@ -29,7 +29,8 @@ import { dirname, join } from "node:path";
29
29
  * enabledByDefault: boolean,
30
30
  * findNearestConfig: (startPath: string) => Promise<{root: string, marker: string} | null>,
31
31
  * parseProjectName: (marker: string, markerContent: string, rootDirName: string) => string,
32
- * resolveCommentSyntax: (filePath: string) => ({kind: "block" | "line" | "html", linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string} | null),
32
+ * resolveCommentSyntax: (filePath: string) => ({kind: "block" | "line" | "html", linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string} | null),
33
+ * resolvePreservedPrefix?: (filePath: string, content: string) => string,
33
34
  * priority?: number
34
35
  * }} DetectorProfile
35
36
  */
@@ -63,9 +64,9 @@ const detectorMap = new Map(DETECTOR_PROFILES.map((detector) => [detector.id, de
63
64
 
64
65
  /**
65
66
  * Applies runtime syntax overrides to a detector-provided syntax descriptor.
66
- * @param {{kind: "block" | "line" | "html", linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} syntax - Base syntax descriptor.
67
- * @param {{ linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string } | undefined} override - Override descriptor.
68
- * @returns {{kind: "block" | "line" | "html", linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} Effective descriptor.
67
+ * @param {{kind: "block" | "line" | "html", linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} syntax - Base syntax descriptor.
68
+ * @param {{ linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string } | undefined} override - Override descriptor.
69
+ * @returns {{kind: "block" | "line" | "html", linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} Effective descriptor.
69
70
  */
70
71
  function applySyntaxOverride(syntax, override) {
71
72
  if (!override || typeof override !== "object") {
@@ -75,7 +76,8 @@ function applySyntaxOverride(syntax, override) {
75
76
  if (syntax.kind === "line") {
76
77
  return {
77
78
  ...syntax,
78
- linePrefix: typeof override.linePrefix === "string" && override.linePrefix.length > 0 ? override.linePrefix : syntax.linePrefix
79
+ linePrefix: typeof override.linePrefix === "string" && override.linePrefix.length > 0 ? override.linePrefix : syntax.linePrefix,
80
+ lineSeparator: typeof override.lineSeparator === "string" ? override.lineSeparator : syntax.lineSeparator
79
81
  };
80
82
  }
81
83
 
@@ -138,8 +140,8 @@ export function getDetectorById(id) {
138
140
  /**
139
141
  * Resolves comment syntax for a file path using detector-specific templates.
140
142
  * @param {string} filePath - File path.
141
- * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [options={}] - Runtime options.
142
- * @returns {{kind: "block" | "line" | "html", linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} Syntax descriptor.
143
+ * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [options={}] - Runtime options.
144
+ * @returns {{kind: "block" | "line" | "html", linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} Syntax descriptor.
143
145
  */
144
146
  export function getCommentSyntaxForFile(filePath, options = {}) {
145
147
  const extension = extname(filePath).toLowerCase();
@@ -158,3 +160,28 @@ export function getCommentSyntaxForFile(filePath, options = {}) {
158
160
 
159
161
  return { kind: "block", blockStart: "/**", blockLinePrefix: " *\t", blockEnd: " */" };
160
162
  }
163
+
164
+ /**
165
+ * Resolves detector-specific leading content that must be preserved above inserted headers.
166
+ * @param {string} filePath - File path.
167
+ * @param {string} content - Full file content.
168
+ * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[] }} [options={}] - Runtime options.
169
+ * @returns {string} Preserved prefix (possibly empty).
170
+ */
171
+ export function getPreservedPrefixForFile(filePath, content, options = {}) {
172
+ const extension = extname(filePath).toLowerCase();
173
+ const detectors = getEnabledDetectors(options);
174
+ for (const detector of detectors) {
175
+ if (!detector.extensions.includes(extension)) {
176
+ continue;
177
+ }
178
+
179
+ if (typeof detector.resolvePreservedPrefix === "function") {
180
+ return detector.resolvePreservedPrefix(filePath, content);
181
+ }
182
+
183
+ return "";
184
+ }
185
+
186
+ return "";
187
+ }
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/detectors/node.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
 
@@ -45,6 +58,17 @@ function resolveNodeCommentSyntax(filePath) {
45
58
  return null;
46
59
  }
47
60
 
61
+ /**
62
+ * Resolves preserved leading prefix for Node files (for example: shebang line).
63
+ * @param {string} _filePath - File path.
64
+ * @param {string} content - File content.
65
+ * @returns {string} Preserved prefix.
66
+ */
67
+ function resolveNodePreservedPrefix(_filePath, content) {
68
+ const shebangMatch = content.match(/^#!.*\b(node|bun|deno|tsx|ts-node)\b.*(?:\r?\n|$)/);
69
+ return shebangMatch ? shebangMatch[0] : "";
70
+ }
71
+
48
72
  export const detector = {
49
73
  id: "node",
50
74
  priority: 100,
@@ -57,6 +81,9 @@ export const detector = {
57
81
  parseProjectName(_marker, markerContent, rootDirName) {
58
82
  return parseNodeProjectName(markerContent, rootDirName);
59
83
  },
84
+ resolvePreservedPrefix(filePath, content) {
85
+ return resolveNodePreservedPrefix(filePath, content);
86
+ },
60
87
  resolveCommentSyntax(filePath) {
61
88
  return resolveNodeCommentSyntax(filePath);
62
89
  }
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/detectors/php.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/python.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
 
@@ -23,6 +36,17 @@ function parsePythonProjectName(markerContent, rootDirName) {
23
36
  return rootDirName;
24
37
  }
25
38
 
39
+ /**
40
+ * Resolves preserved leading prefix for Python files (for example: shebang line).
41
+ * @param {string} _filePath - File path.
42
+ * @param {string} content - File content.
43
+ * @returns {string} Preserved prefix.
44
+ */
45
+ function resolvePythonPreservedPrefix(_filePath, content) {
46
+ const shebangMatch = content.match(/^#!.*\bpython(?:\d+(?:\.\d+)*)?\b.*(?:\r?\n|$)/);
47
+ return shebangMatch ? shebangMatch[0] : "";
48
+ }
49
+
26
50
  export const detector = {
27
51
  id: "python",
28
52
  priority: 80,
@@ -35,6 +59,9 @@ export const detector = {
35
59
  parseProjectName(_marker, markerContent, rootDirName) {
36
60
  return parsePythonProjectName(markerContent, rootDirName);
37
61
  },
62
+ resolvePreservedPrefix(filePath, content) {
63
+ return resolvePythonPreservedPrefix(filePath, content);
64
+ },
38
65
  resolveCommentSyntax(filePath) {
39
66
  const extension = extname(filePath).toLowerCase();
40
67
  if (extensions.includes(extension)) {
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/detectors/rust.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/shared.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 { dirname, join, resolve } from "node:path";
2
15
  import { pathExists } from "../utils/fs.mjs";
3
16
 
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/detectors/yaml.mjs
4
+ * @Date: 2026-03-01 18:28:31 -08:00 (1772418511)
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-01 19:32:27 -08:00 (1772422347)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2026-2026 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
14
+ import { extname } from "node:path";
15
+ import { findNearestMarker } from "./shared.mjs";
16
+
17
+ /**
18
+ * @fileoverview YAML detector implementation.
19
+ * @module fix-headers/detectors/yaml
20
+ */
21
+
22
+ const markers = ["package.json", ".git"];
23
+ const extensions = [".yaml", ".yml"];
24
+
25
+ /**
26
+ * Parses YAML project name from nearest marker content when available.
27
+ * @param {string} marker - Marker filename.
28
+ * @param {string} markerContent - Marker content.
29
+ * @param {string} rootDirName - Fallback root directory name.
30
+ * @returns {string} Project name.
31
+ */
32
+ function parseYamlProjectName(marker, markerContent, rootDirName) {
33
+ if (marker !== "package.json") {
34
+ return rootDirName;
35
+ }
36
+
37
+ try {
38
+ const parsed = JSON.parse(markerContent);
39
+ if (typeof parsed.name === "string" && parsed.name.trim().length > 0) {
40
+ return parsed.name.trim();
41
+ }
42
+ return rootDirName;
43
+ } catch {
44
+ return rootDirName;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Resolves comment syntax for YAML file extensions.
50
+ * @param {string} filePath - File path.
51
+ * @returns {{kind: "line", linePrefix: string} | null} Syntax descriptor.
52
+ */
53
+ function resolveYamlCommentSyntax(filePath) {
54
+ const extension = extname(filePath).toLowerCase();
55
+ if (extensions.includes(extension)) {
56
+ return {
57
+ kind: "line",
58
+ linePrefix: "#"
59
+ };
60
+ }
61
+ return null;
62
+ }
63
+
64
+ export const detector = {
65
+ id: "yaml",
66
+ priority: 60,
67
+ markers,
68
+ extensions,
69
+ enabledByDefault: true,
70
+ findNearestConfig(startPath) {
71
+ return findNearestMarker(startPath, markers);
72
+ },
73
+ parseProjectName(marker, markerContent, rootDirName) {
74
+ return parseYamlProjectName(marker, markerContent, rootDirName);
75
+ },
76
+ resolveCommentSyntax(filePath) {
77
+ return resolveYamlCommentSyntax(filePath);
78
+ }
79
+ };
@@ -2,13 +2,13 @@
2
2
  * @Project: @cldmv/fix-headers
3
3
  * @Filename: /src/fix-header.mjs
4
4
  * @Date: 2026-03-01 13:34:00 -08:00 (1772400840)
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 14:39:14 -08:00 (1772404754)
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
  import { fixHeaders as fixHeadersCore } from "./core/fix-headers.mjs";
@@ -1,4 +1,18 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/header/parser.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 { DEFAULT_MAX_HEADER_SCAN_LINES } from "../constants.mjs";
15
+ import { getPreservedPrefixForFile } from "../detectors/index.mjs";
2
16
  import { getHeaderSyntaxForFile } from "./syntax.mjs";
3
17
 
4
18
  /**
@@ -16,41 +30,85 @@ function escapeRegex(text) {
16
30
  }
17
31
 
18
32
  /**
19
- * Finds the first top-level project header block in a file.
33
+ * Splits detector-defined preserved prefix from file content when present.
34
+ * @param {string} filePath - File path used for detector selection.
20
35
  * @param {string} content - File content.
21
- * @param {string} [filePath=""] - File path used for syntax selection.
22
- * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [syntaxOptions={}] - Syntax resolution options.
23
- * @returns {{start: number, end: number} | null} Header location.
36
+ * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [syntaxOptions={}] - Syntax/detector options.
37
+ * @returns {{prefix: string, body: string}} Shebang prefix and remaining body.
24
38
  */
25
- export function findProjectHeader(content, filePath = "", syntaxOptions = {}) {
26
- const scanLines = content.split("\n").slice(0, DEFAULT_MAX_HEADER_SCAN_LINES).join("\n");
27
- const syntax = getHeaderSyntaxForFile(filePath, syntaxOptions);
39
+ function splitPreservedPrefix(filePath, content, syntaxOptions = {}) {
40
+ const prefix = getPreservedPrefixForFile(filePath, content, syntaxOptions);
41
+ if (prefix.length === 0) {
42
+ return { prefix: "", body: content };
43
+ }
44
+
45
+ return {
46
+ prefix,
47
+ body: content.slice(prefix.length)
48
+ };
49
+ }
28
50
 
29
- let match = null;
51
+ /**
52
+ * Matches a top-of-content header block for a given syntax.
53
+ * @param {string} content - Content slice to match from start.
54
+ * @param {{kind: "block" | "line" | "html", linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} syntax - Header syntax descriptor.
55
+ * @returns {RegExpMatchArray | null} Matched header segment.
56
+ */
57
+ function matchHeaderSegment(content, syntax) {
30
58
  if (syntax.kind === "html") {
31
59
  const blockStart = escapeRegex(syntax.blockStart || "<!--");
32
60
  const blockEnd = escapeRegex(syntax.blockEnd || "-->");
33
- match = scanLines.match(new RegExp(`^${blockStart}[\\s\\S]*?${blockEnd}\\n*`));
34
- } else if (syntax.kind === "line") {
61
+ return content.match(new RegExp(`^${blockStart}[\\s\\S]*?${blockEnd}\\n*`));
62
+ }
63
+
64
+ if (syntax.kind === "line") {
35
65
  const linePrefix = escapeRegex(syntax.linePrefix || "#");
36
- match = scanLines.match(new RegExp(`^(?:${linePrefix}.*\\n)+\\n*`));
37
- } else {
38
- const blockStart = escapeRegex(syntax.blockStart || "/**");
39
- const blockEnd = escapeRegex(syntax.blockEnd || " */");
40
- match = scanLines.match(new RegExp(`^${blockStart}[\\s\\S]*?${blockEnd}\\n*`));
66
+ return content.match(new RegExp(`^(?:${linePrefix}.*\\n)+\\n*`));
41
67
  }
42
68
 
69
+ const blockStart = escapeRegex(syntax.blockStart || "/**");
70
+ const blockEnd = escapeRegex(syntax.blockEnd || " */");
71
+ return content.match(new RegExp(`^${blockStart}[\\s\\S]*?${blockEnd}\\n*`));
72
+ }
73
+
74
+ /**
75
+ * Finds the first top-level project header block in a file.
76
+ * @param {string} content - File content.
77
+ * @param {string} [filePath=""] - File path used for syntax selection.
78
+ * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [syntaxOptions={}] - Syntax resolution options.
79
+ * @returns {{start: number, end: number} | null} Header location.
80
+ */
81
+ export function findProjectHeader(content, filePath = "", syntaxOptions = {}) {
82
+ const { prefix, body } = splitPreservedPrefix(filePath, content, syntaxOptions);
83
+ const scanLines = body.split("\n").slice(0, DEFAULT_MAX_HEADER_SCAN_LINES).join("\n");
84
+ const syntax = getHeaderSyntaxForFile(filePath, syntaxOptions);
85
+ const match = matchHeaderSegment(scanLines, syntax);
86
+
43
87
  if (!match) {
44
88
  return null;
45
89
  }
46
90
 
47
- if (!match[0].includes("@Project:")) {
91
+ let offset = 0;
92
+ let end = 0;
93
+ while (true) {
94
+ const remaining = scanLines.slice(offset);
95
+ const segment = matchHeaderSegment(remaining, syntax);
96
+
97
+ if (!segment || !segment[0].includes("@Project:")) {
98
+ break;
99
+ }
100
+
101
+ offset += segment[0].length;
102
+ end = offset;
103
+ }
104
+
105
+ if (end === 0) {
48
106
  return null;
49
107
  }
50
108
 
51
109
  return {
52
- start: 0,
53
- end: match[0].length
110
+ start: prefix.length,
111
+ end: prefix.length + end
54
112
  };
55
113
  }
56
114
 
@@ -59,13 +117,14 @@ export function findProjectHeader(content, filePath = "", syntaxOptions = {}) {
59
117
  * @param {string} content - Original file content.
60
118
  * @param {string} newHeader - Generated header text.
61
119
  * @param {string} [filePath=""] - File path used for syntax selection.
62
- * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [syntaxOptions={}] - Syntax resolution options.
120
+ * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [syntaxOptions={}] - Syntax resolution options.
63
121
  * @returns {{nextContent: string, changed: boolean}} Updated content result.
64
122
  */
65
123
  export function replaceOrInsertHeader(content, newHeader, filePath = "", syntaxOptions = {}) {
66
124
  const existing = findProjectHeader(content, filePath, syntaxOptions);
67
125
  if (!existing) {
68
- const nextContent = `${newHeader}\n\n${content.replace(/^\n+/, "")}`;
126
+ const { prefix, body } = splitPreservedPrefix(filePath, content, syntaxOptions);
127
+ const nextContent = `${prefix}${newHeader}\n\n${body.replace(/^\n+/, "")}`;
69
128
  return { nextContent, changed: nextContent !== content };
70
129
  }
71
130
 
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/header/syntax.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 { getCommentSyntaxForFile } from "../detectors/index.mjs";
3
16
 
@@ -10,6 +23,7 @@ import { getCommentSyntaxForFile } from "../detectors/index.mjs";
10
23
  * @typedef {{
11
24
  * kind: "block" | "line" | "html",
12
25
  * linePrefix?: string,
26
+ * lineSeparator?: string,
13
27
  * blockStart?: string,
14
28
  * blockLinePrefix?: string,
15
29
  * blockEnd?: string
@@ -19,7 +33,7 @@ import { getCommentSyntaxForFile } from "../detectors/index.mjs";
19
33
  /**
20
34
  * Resolves comment syntax for a file path based on extension.
21
35
  * @param {string} filePath - Absolute or relative file path.
22
- * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [options={}] - Syntax resolution options.
36
+ * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> }} [options={}] - Syntax resolution options.
23
37
  * @returns {HeaderSyntax} Header syntax descriptor.
24
38
  */
25
39
  export function getHeaderSyntaxForFile(filePath, options = {}) {
@@ -39,7 +53,8 @@ export function getHeaderSyntaxForFile(filePath, options = {}) {
39
53
  */
40
54
  export function renderHeaderLines(syntax, lines) {
41
55
  if (syntax.kind === "line") {
42
- return lines.map((line) => `${syntax.linePrefix || "#"}\t${line}`).join("\n");
56
+ const lineSeparator = typeof syntax.lineSeparator === "string" ? syntax.lineSeparator : "\t";
57
+ return lines.map((line) => `${syntax.linePrefix || "#"}${lineSeparator}${line}`).join("\n");
43
58
  }
44
59
 
45
60
  const blockStart = syntax.blockStart || (syntax.kind === "html" ? "<!--" : "/**");
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/header/template.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 { relative } from "node:path";
2
15
  import { getHeaderSyntaxForFile, renderHeaderLines } from "./syntax.mjs";
3
16
 
@@ -11,8 +24,12 @@ import { getHeaderSyntaxForFile, renderHeaderLines } from "./syntax.mjs";
11
24
  * @param {{
12
25
  * absoluteFilePath: string,
13
26
  * language?: string,
14
- * syntaxOptions?: { language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> },
27
+ * syntaxOptions?: { language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record<string, { linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string }> },
15
28
  * projectRoot: string,
29
+ * createdByName?: string,
30
+ * createdByEmail?: string,
31
+ * lastModifiedByName?: string,
32
+ * lastModifiedByEmail?: string,
16
33
  * projectName: string,
17
34
  * authorName: string,
18
35
  * authorEmail: string,
@@ -27,17 +44,21 @@ import { getHeaderSyntaxForFile, renderHeaderLines } from "./syntax.mjs";
27
44
  export function buildHeader(data) {
28
45
  const relativePath = `/${relative(data.projectRoot, data.absoluteFilePath).replace(/\\/g, "/")}`;
29
46
  const syntax = getHeaderSyntaxForFile(data.absoluteFilePath, data.syntaxOptions || { language: data.language });
47
+ const createdByName = data.createdByName || data.authorName;
48
+ const createdByEmail = data.createdByEmail || data.authorEmail;
49
+ const lastModifiedByName = data.lastModifiedByName || data.authorName;
50
+ const lastModifiedByEmail = data.lastModifiedByEmail || data.authorEmail;
30
51
  const headerLines = [
31
52
  `@Project: ${data.projectName}`,
32
53
  `@Filename: ${relativePath}`,
33
54
  `@Date: ${data.createdAt.date} (${data.createdAt.timestamp})`,
34
- `@Author: ${data.authorName}`,
35
- `@Email: <${data.authorEmail}>`,
55
+ `@Author: ${createdByName}`,
56
+ `@Email: <${createdByEmail}>`,
36
57
  "-----",
37
- `@Last modified by: ${data.authorName} (${data.authorEmail})`,
58
+ `@Last modified by: ${lastModifiedByName} (${lastModifiedByEmail})`,
38
59
  `@Last modified time: ${data.lastModifiedAt.date} (${data.lastModifiedAt.timestamp})`,
39
60
  "-----",
40
- `@Copyright: Copyright (c) ${data.copyrightStartYear}-${data.currentYear} ${data.companyName}. All rights reserved.`
61
+ `@Copyright: Copyright (c) ${data.copyrightStartYear}-${data.currentYear} ${data.companyName} All rights reserved.`
41
62
  ];
42
63
 
43
64
  return renderHeaderLines(syntax, headerLines);
package/src/utils/fs.mjs CHANGED
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/utils/fs.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 { access, readdir, readFile, stat } from "node:fs/promises";
2
15
  import { constants as fsConstants } from "node:fs";
3
16
  import { dirname, join, resolve } from "node:path";
package/src/utils/git.mjs CHANGED
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/utils/git.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 { execFile } from "node:child_process";
2
15
  import { promisify } from "node:util";
3
16
 
@@ -24,14 +37,39 @@ export async function runGit(cwd, args) {
24
37
  }
25
38
  }
26
39
 
40
+ /**
41
+ * Parses a signer UID string into author name and optional email.
42
+ * @param {string} signerUid - Raw signer UID (for example: "Name (Comment) <email@example.com>").
43
+ * @returns {{authorName: string | null, authorEmail: string | null}} Parsed signer identity.
44
+ */
45
+ function parseSignerUid(signerUid) {
46
+ const trimmed = signerUid.trim();
47
+ if (trimmed.length === 0) {
48
+ return { authorName: null, authorEmail: null };
49
+ }
50
+
51
+ const emailMatch = trimmed.match(/<([^>\n]+)>\s*$/);
52
+ const authorEmail = emailMatch?.[1]?.trim() || null;
53
+ const authorName = (emailMatch ? trimmed.slice(0, emailMatch.index) : trimmed).trim() || null;
54
+
55
+ return { authorName, authorEmail };
56
+ }
57
+
27
58
  /**
28
59
  * Detects git author name and email from config or commit history.
29
60
  * @param {string} cwd - Project directory.
61
+ * @param {{useGpgSignerAuthor?: boolean}} [options={}] - Detection options.
30
62
  * @returns {Promise<{authorName: string | null, authorEmail: string | null}>} Author information.
31
63
  */
32
- export async function detectGitAuthor(cwd) {
64
+ export async function detectGitAuthor(cwd, options = {}) {
33
65
  let authorName = await runGit(cwd, ["config", "--get", "user.name"]);
34
66
  let authorEmail = await runGit(cwd, ["config", "--get", "user.email"]);
67
+ if (options.useGpgSignerAuthor === true) {
68
+ const signerUid = await runGit(cwd, ["log", "-1", "--format=%GS"]);
69
+ const signerIdentity = parseSignerUid(signerUid || "");
70
+ authorName = signerIdentity.authorName || authorName;
71
+ authorEmail = authorEmail || signerIdentity.authorEmail;
72
+ }
35
73
 
36
74
  if (!authorName || !authorEmail) {
37
75
  const fallback = await runGit(cwd, ["log", "-1", "--format=%an|%ae"]);
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @Project: @cldmv/fix-headers
3
+ * @Filename: /src/utils/time.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 Date/time helpers used for header timestamp formatting.
3
16
  * @module fix-headers/utils/time