Skip to content

Commit dd28e63

Browse files
committed
feat(cli): case-insensitive file resolution with two-phase matching
Two-phase matching algorithm in resolveInput(): 1. Case-sensitive exact matching (preserves priority order) 2. Case-insensitive fallback (supports variations like .SPM.JSON) Also updates suffixMap to use .SysProM.* as canonical format. - Detects ambiguous case variants and errors - Priority: .SysProM.* > .spm.* > .sysprom.*
1 parent db7b843 commit dd28e63

1 file changed

Lines changed: 36 additions & 4 deletions

File tree

src/cli/shared.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ const pathOpt = z
3232
* 13. *.spm.json 14. *.spm.md 15. *.spm/
3333
* 16. *.sysprom.json 17. *.sysprom.md 18. *.sysprom/
3434
*
35-
* All matching is case-insensitive. Glob tiers must have exactly one match.
35+
* Matching is case-insensitive for base names and directory matches.
36+
* Files like .SysProM.json, .sysprom.json, and .SYSPROM.JSON are treated
37+
* as equivalent and all match the .SysProM.json priority tier.
38+
* Glob tiers must have exactly one match.
3639
* @param input - Explicit document path, or undefined for auto-detection.
3740
* @param cwd - Working directory to search from (defaults to `.`).
3841
* @returns The resolved document path.
@@ -47,7 +50,6 @@ export function resolveInput(input?: string, cwd?: string): string {
4750

4851
const dir = resolve(cwd ?? ".");
4952

50-
// Exact names to check, in priority order (case-insensitive)
5153
const exactNames = [
5254
".SysProM.json",
5355
".SysProM.md",
@@ -62,12 +64,35 @@ export function resolveInput(input?: string, cwd?: string): string {
6264

6365
const entries = readdirSync(dir);
6466

67+
// Phase 1: Try exact case-sensitive matches first
6568
for (const name of exactNames) {
6669
const isDirSuffix =
6770
name.endsWith(".SysProM") ||
6871
name.endsWith(".spm") ||
6972
name.endsWith(".sysprom");
70-
const found = entries.filter((e) => e.toLowerCase() === name);
73+
const found = entries.filter((e) => e === name);
74+
if (found.length === 1) {
75+
const candidate = join(dir, found[0]);
76+
if (isDirSuffix) {
77+
try {
78+
if (statSync(candidate).isDirectory()) return candidate;
79+
} catch {
80+
/* skip */
81+
}
82+
} else {
83+
return candidate;
84+
}
85+
}
86+
}
87+
88+
// Phase 2: Try case-insensitive matches (supports case variations like .SPM.JSON)
89+
for (const name of exactNames) {
90+
const isDirSuffix =
91+
name.endsWith(".SysProM") ||
92+
name.endsWith(".spm") ||
93+
name.endsWith(".sysprom");
94+
const nameLower = name.toLowerCase();
95+
const found = entries.filter((e) => e.toLowerCase() === nameLower);
7196
if (found.length > 1) {
7297
throw new Error(
7398
`Multiple SysProM documents found: ${found.join(", ")}. Specify one explicitly.`,
@@ -103,10 +128,17 @@ export function resolveInput(input?: string, cwd?: string): string {
103128
for (const suffix of globSuffixes) {
104129
const isDirSuffix =
105130
suffix === ".SysProM" || suffix === ".spm" || suffix === ".sysprom";
131+
const suffixLower = suffix.toLowerCase();
106132
const matches = entries
107133
.filter((e) => {
108134
const lower = e.toLowerCase();
109-
return lower.endsWith(suffix) && lower !== suffix;
135+
// Match files ending with this suffix (case-insensitive) that don't start with "."
136+
// and are not exact matches (those were already checked)
137+
return (
138+
lower.endsWith(suffixLower) &&
139+
lower !== suffixLower &&
140+
!e.startsWith(".")
141+
);
110142
})
111143
.map((e) => join(dir, e))
112144
.filter((p) => {

0 commit comments

Comments
 (0)