-
Notifications
You must be signed in to change notification settings - Fork 27
feat(setpath, use): autocomplete + tilde expansion #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,23 +1,90 @@ | ||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; | ||
| import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction } from 'discord.js'; | ||
| import { readdirSync, existsSync } from 'node:fs'; | ||
| import { join, basename } from 'node:path'; | ||
| import { homedir } from 'node:os'; | ||
| import * as dataStore from '../services/dataStore.js'; | ||
|
|
||
| // Scan ~/Projects (one level deep) for git repositories. | ||
| // Returns a list of absolute paths, or [] if the dir is missing/unreadable. | ||
| function scanGitRepos(): string[] { | ||
| const base = join(homedir(), 'Projects'); | ||
| try { | ||
| return readdirSync(base, { withFileTypes: true }) | ||
| .filter(e => e.isDirectory()) | ||
| .map(e => join(base, e.name)) | ||
| .filter(p => existsSync(join(p, '.git'))); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| // Expand a leading ~ to the user's home directory. | ||
| // Discord passes paths as raw strings (no shell), so the bot must do this itself. | ||
| function expandTilde(p: string): string { | ||
| if (!p) return p; | ||
| if (p === '~') return homedir(); | ||
| if (p.startsWith('~/')) return join(homedir(), p.slice(2)); | ||
| return p; | ||
| } | ||
|
|
||
| export const setpath = { | ||
| data: new SlashCommandBuilder() | ||
| .setName('setpath') | ||
| .setDescription('Register a project path') | ||
| .addStringOption(option => | ||
| option.setName('alias') | ||
| .setDescription('Project alias') | ||
| .setRequired(true)) | ||
| .setRequired(true) | ||
| .setAutocomplete(true)) | ||
| .addStringOption(option => | ||
| option.setName('path') | ||
| .setDescription('Project path') | ||
| .setRequired(true)), | ||
|
|
||
| .setDescription('Project path (autocomplete from ~/Projects)') | ||
| .setRequired(true) | ||
| .setAutocomplete(true)), | ||
|
|
||
| async autocomplete(interaction: AutocompleteInteraction) { | ||
| const focused = interaction.options.getFocused(true); | ||
| const query = (focused.value || '').toLowerCase(); | ||
| const home = homedir(); | ||
|
|
||
| if (focused.name === 'path') { | ||
| const repos = scanGitRepos(); | ||
| const matches = repos | ||
| .filter(p => p.toLowerCase().includes(query)) | ||
| .slice(0, 25) | ||
| .map(p => ({ | ||
| name: p.replace(home, '~').slice(0, 100), | ||
| value: p | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] The displayed choice |
||
| })); | ||
| await interaction.respond(matches); | ||
| return; | ||
| } | ||
|
|
||
| if (focused.name === 'alias') { | ||
| // Suggest existing aliases (in case the user is overwriting) | ||
| // and aliases derived from repo dir basenames (with `client-` stripped). | ||
| const suggestions = new Set<string>(); | ||
| dataStore.getProjects().forEach(p => suggestions.add(p.alias)); | ||
| scanGitRepos().forEach(p => { | ||
| const a = basename(p).toLowerCase().replace(/^client-/, ''); | ||
| suggestions.add(a); | ||
| }); | ||
| const matches = [...suggestions] | ||
| .filter(a => a.toLowerCase().includes(query)) | ||
| .slice(0, 25) | ||
| .map(a => ({ name: a.slice(0, 100), value: a })); | ||
| await interaction.respond(matches); | ||
| return; | ||
| } | ||
|
|
||
| await interaction.respond([]); | ||
| }, | ||
|
|
||
| async execute(interaction: ChatInputCommandInteraction) { | ||
| const alias = interaction.options.getString('alias', true); | ||
| const path = interaction.options.getString('path', true); | ||
|
|
||
| const rawPath = interaction.options.getString('path', true); | ||
| const path = expandTilde(rawPath); | ||
|
|
||
| dataStore.addProject(alias, path); | ||
| await interaction.reply(`✅ Project '${alias}' registered: ${path}`); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] This autocomplete path now exposes local project metadata before the allowlist check runs.
handleInteractionprocesses autocomplete interactions beforeisAuthorized, so an unauthorized user in a shared Discord server can type/setpathand receive scanned~/Projectsrepo names/paths. Please gate autocomplete interactions with the same allowlist check and return an empty suggestion list for unauthorized users.