Skip to content

Commit 0cfe02e

Browse files
committed
fix: fuzzy-resolve org slug before slash + fix zsh completion context
Address two review findings: 1. Seer: When user types 'senry/', fuzzy-resolve the org part to 'sentry' before querying projects. Previously the exact-match SQL query would find nothing for the typo'd org slug. 2. BugBot: In zsh, _arguments -C with '*::arg:->args' resets $words to only remaining positional args, stripping command/subcommand. Fixed by using $line[1] and $line[2] (set by _arguments -C) to pass the full command context to __complete.
1 parent 69c62ee commit 0cfe02e

File tree

3 files changed

+69
-21
lines changed

3 files changed

+69
-21
lines changed

src/lib/complete.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,17 @@ export async function completeOrgSlashProject(
184184
return [...orgCompletions, ...aliasCompletions];
185185
}
186186

187-
// Has slash — complete project within the org
187+
// Has slash — complete project within the org.
188+
// Fuzzy-resolve the org slug first so that "senry/" still finds "sentry" projects.
188189
const orgPart = partial.slice(0, slashIdx);
189190
const projectPart = partial.slice(slashIdx + 1);
190191

191-
return completeProjectSlugs(projectPart, orgPart);
192+
const resolvedOrg = await fuzzyResolveOrg(orgPart);
193+
if (!resolvedOrg) {
194+
return [];
195+
}
196+
197+
return completeProjectSlugs(projectPart, resolvedOrg);
192198
}
193199

194200
/**
@@ -248,6 +254,27 @@ export async function completeProjectSlugs(
248254
}));
249255
}
250256

257+
/**
258+
* Fuzzy-resolve an org slug to its canonical form.
259+
*
260+
* When the user types `senry/`, we need to find the actual cached org
261+
* slug `sentry` so we can query projects for it. Returns the best
262+
* fuzzy match, or the original slug if it matches exactly.
263+
*
264+
* @param orgPart - The potentially misspelled org slug
265+
* @returns The resolved org slug, or undefined if no match
266+
*/
267+
async function fuzzyResolveOrg(orgPart: string): Promise<string | undefined> {
268+
const orgs = await getCachedOrganizations();
269+
if (orgs.length === 0) {
270+
return;
271+
}
272+
273+
const slugs = orgs.map((o) => o.slug);
274+
const matched = fuzzyMatch(orgPart, slugs, { maxResults: 1 });
275+
return matched[0];
276+
}
277+
251278
/**
252279
* Complete project aliases (e.g., `A`, `B` from monorepo detection).
253280
*

src/lib/completions.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -401,24 +401,9 @@ export function generateZshCompletion(binaryName: string): string {
401401
# zsh completion for ${binaryName}
402402
# Auto-generated from command definitions
403403
404-
# Dynamic completer for positional values (org slugs, project names)
405-
_${binaryName}_dynamic() {
406-
local -a completions
407-
local line
408-
while IFS=$'\\t' read -r value desc; do
409-
if [[ -n "$desc" ]]; then
410-
completions+=("\${value}:\${desc}")
411-
else
412-
completions+=("\${value}")
413-
fi
414-
done < <(${binaryName} __complete \${words[2,-1]} 2>/dev/null)
415-
if [[ \${#completions} -gt 0 ]]; then
416-
_describe -t values 'value' completions
417-
fi
418-
}
419-
420404
_${binaryName}() {
421405
local -a commands
406+
local curcontext="$curcontext" state line
422407
commands=(
423408
${topLevelItems}
424409
)
@@ -435,13 +420,25 @@ ${subArrays}
435420
_describe -t commands 'command' commands
436421
;;
437422
subcommand)
438-
case "$words[1]" in
423+
case "$line[1]" in
439424
${caseBranches}
440425
esac
441426
;;
442427
args)
443-
# Dynamic completion for positional args
444-
_${binaryName}_dynamic
428+
# Dynamic completion for positional args (org slugs, project names)
429+
# In the args state, $line[1] and $line[2] hold the parsed command
430+
# and subcommand. Pass them to __complete for context detection.
431+
local -a completions
432+
while IFS=$'\\t' read -r value desc; do
433+
if [[ -n "$desc" ]]; then
434+
completions+=("\${value}:\${desc}")
435+
else
436+
completions+=("\${value}")
437+
fi
438+
done < <(${binaryName} __complete "$line[1]" "$line[2]" "\${words[@]}" 2>/dev/null)
439+
if (( \${#completions} )); then
440+
_describe -t values 'value' completions
441+
fi
445442
;;
446443
esac
447444
}

test/lib/complete.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,30 @@ describe("completeOrgSlashProject", () => {
169169
]);
170170
});
171171

172+
test("with fuzzy org slug before slash resolves to correct org", async () => {
173+
await seedOrgs([{ slug: "sentry", name: "Sentry" }]);
174+
await seedProjects([
175+
{
176+
orgId: "1",
177+
projectId: "10",
178+
orgSlug: "sentry",
179+
projectSlug: "cli",
180+
projectName: "CLI",
181+
},
182+
]);
183+
184+
// "senry/" has a typo in the org — should fuzzy-resolve to "sentry"
185+
const result = await completeOrgSlashProject("senry/");
186+
expect(result).toHaveLength(1);
187+
expect(result[0].value).toBe("sentry/cli");
188+
});
189+
190+
test("with unresolvable org slug before slash returns empty", async () => {
191+
await seedOrgs([{ slug: "sentry", name: "Sentry" }]);
192+
const result = await completeOrgSlashProject("zzzzzzz/");
193+
expect(result).toEqual([]);
194+
});
195+
172196
test("with slash and partial project filters", async () => {
173197
await seedOrgs([{ slug: "my-org", name: "My Org" }]);
174198
await seedProjects([

0 commit comments

Comments
 (0)