Skip to content

Commit 61011ec

Browse files
committed
feat: case-insensitive add-on lookup with typo suggestions
1 parent 46a4903 commit 61011ec

File tree

4 files changed

+72
-6
lines changed

4 files changed

+72
-6
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@tanstack/cli": patch
3+
"@tanstack/create": patch
4+
---
5+
6+
Add case-insensitive add-on ID matching and "did you mean?" suggestions for typos

packages/cli/src/cli.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@ export function cli({
122122
getFrameworkByName(options.framework || defaultFramework || 'React')!,
123123
defaultMode,
124124
)
125-
const addOn = addOns.find((a) => a.id === options.addonDetails)
125+
const addOn =
126+
addOns.find((a) => a.id === options.addonDetails) ??
127+
addOns.find(
128+
(a) =>
129+
a.id.toLowerCase() === options.addonDetails!.toLowerCase(),
130+
)
126131
if (!addOn) {
127132
console.error(`Add-on '${options.addonDetails}' not found`)
128133
process.exit(1)

packages/cli/src/mcp.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,12 @@ function createServer({
8585
},
8686
async ({ framework: frameworkName, addOnId }) => {
8787
const framework = getFrameworkByName(frameworkName)!
88-
const addOn = framework
89-
.getAddOns()
90-
.find((a) => a.id === addOnId)
88+
const allAddOns = framework.getAddOns()
89+
const addOn =
90+
allAddOns.find((a) => a.id === addOnId) ??
91+
allAddOns.find(
92+
(a) => a.id.toLowerCase() === addOnId.toLowerCase(),
93+
)
9194

9295
if (!addOn) {
9396
return {

packages/create/src/add-ons.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,24 @@ export async function finalizeAddOns(
2525

2626
for (const addOnID of finalAddOnIDs) {
2727
let addOn: AddOn | undefined
28-
const localAddOn = addOns.find((a) => a.id === addOnID)
28+
const localAddOn =
29+
addOns.find((a) => a.id === addOnID) ??
30+
addOns.find((a) => a.id.toLowerCase() === addOnID.toLowerCase())
2931
if (localAddOn) {
3032
addOn = loadAddOn(localAddOn)
33+
if (localAddOn.id !== addOnID) {
34+
// Replace the mistyped ID with the canonical one
35+
finalAddOnIDs.delete(addOnID)
36+
finalAddOnIDs.add(localAddOn.id)
37+
}
3138
} else if (addOnID.startsWith('http')) {
3239
addOn = await loadRemoteAddOn(addOnID)
3340
addOns.push(addOn)
3441
} else {
35-
throw new Error(`Add-on ${addOnID} not found`)
42+
const suggestion = findClosestAddOn(addOnID, addOns)
43+
throw new Error(
44+
`Add-on ${addOnID} not found${suggestion ? `. Did you mean "${suggestion}"?` : ''}`,
45+
)
3646
}
3747

3848
for (const dependsOn of addOn.dependsOn || []) {
@@ -55,6 +65,48 @@ function loadAddOn(addOn: AddOn): AddOn {
5565
return addOn
5666
}
5767

68+
function findClosestAddOn(
69+
input: string,
70+
addOns: Array<AddOn>,
71+
): string | undefined {
72+
const inputLower = input.toLowerCase()
73+
let bestMatch: string | undefined
74+
let bestDistance = Infinity
75+
76+
for (const addOn of addOns) {
77+
const d = levenshtein(inputLower, addOn.id.toLowerCase())
78+
if (d < bestDistance) {
79+
bestDistance = d
80+
bestMatch = addOn.id
81+
}
82+
}
83+
84+
// Only suggest if the distance is reasonable (less than half the input length)
85+
if (bestMatch && bestDistance <= Math.max(Math.floor(input.length / 2), 2)) {
86+
return bestMatch
87+
}
88+
return undefined
89+
}
90+
91+
function levenshtein(a: string, b: string): number {
92+
const m = a.length
93+
const n = b.length
94+
let prev = Array.from({ length: n + 1 }, (_, j) => j)
95+
96+
for (let i = 1; i <= m; i++) {
97+
const curr = [i]
98+
for (let j = 1; j <= n; j++) {
99+
curr[j] =
100+
a[i - 1] === b[j - 1]
101+
? prev[j - 1]
102+
: 1 + Math.min(prev[j], curr[j - 1], prev[j - 1])
103+
}
104+
prev = curr
105+
}
106+
107+
return prev[n]
108+
}
109+
58110
export function populateAddOnOptionsDefaults(
59111
chosenAddOns: Array<AddOn>,
60112
): Record<string, Record<string, any>> {

0 commit comments

Comments
 (0)