Skip to content

Commit dc0d648

Browse files
plossonclaude
andcommitted
feat: add interactive prompts for human-friendly CLI UX
- Add @inquirer/prompts for interactive select/checkbox prompts - Add interactive profile selection to config export command - Update gchat profile add with interactive type selection - Update jira profile add with interactive site selection - Update sql profile add with interactive database type selection - Add src/utils/interactive.ts with TTY-aware prompt wrappers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 64e868c commit dc0d648

7 files changed

Lines changed: 320 additions & 50 deletions

File tree

bun.lock

Lines changed: 54 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"typescript": "^5.9.3"
4747
},
4848
"dependencies": {
49+
"@inquirer/prompts": "^8.2.0",
4950
"commander": "^14.0.2",
5051
"googleapis": "^169.0.0",
5152
"libsodium-wrappers": "^0.8.1",

src/commands/config.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ import { loadConfig, saveConfig, setEnv, unsetEnv, listEnv } from '../config/con
77
import { getAllCredentials, setAllCredentials } from '../auth/token-store';
88
import { CliError, handleError } from '../utils/errors';
99
import { confirm } from '../utils/stdin';
10-
import type { Config } from '../types/config';
10+
import { isInteractive, interactiveCheckbox, interactiveSelect } from '../utils/interactive';
11+
import type { Config, ServiceName } from '../types/config';
1112
import type { StoredCredentials } from '../types/tokens';
1213

14+
interface ProfileSelection {
15+
service: ServiceName;
16+
profile: string;
17+
}
18+
1319
const ALGORITHM = 'aes-256-gcm';
1420

1521
interface ExportedData {
@@ -94,6 +100,7 @@ export function registerConfigCommands(program: Command): void {
94100
.description('Export configuration and credentials (as environment variables by default, or to a file)')
95101
.option('--key <key>', 'Encryption key (64 hex characters). If not provided, a random key will be generated')
96102
.option('--file <path>', 'Write encrypted config to file instead of outputting AGENTIO_CONFIG')
103+
.option('--all', 'Export all profiles without prompting for selection')
97104
.action(async (options) => {
98105
try {
99106
// Validate key if provided
@@ -115,23 +122,103 @@ export function registerConfigCommands(program: Command): void {
115122
const configData = await loadConfig();
116123
const credentials = await getAllCredentials();
117124

125+
// Build list of all available profiles
126+
const allProfiles: ProfileSelection[] = [];
127+
for (const [service, profiles] of Object.entries(configData.profiles)) {
128+
if (profiles) {
129+
for (const profile of profiles) {
130+
allProfiles.push({ service: service as ServiceName, profile });
131+
}
132+
}
133+
}
134+
135+
if (allProfiles.length === 0) {
136+
throw new CliError(
137+
'NOT_FOUND',
138+
'No profiles configured',
139+
'Add profiles first with: agentio <service> profile add'
140+
);
141+
}
142+
143+
// Determine which profiles to export
144+
let selectedProfiles: ProfileSelection[];
145+
146+
if (options.all || !isInteractive()) {
147+
// Export all profiles
148+
selectedProfiles = allProfiles;
149+
} else {
150+
// Interactive: ask user to select profiles
151+
const exportAll = await interactiveSelect({
152+
message: 'What would you like to export?',
153+
choices: [
154+
{ name: `All profiles (${allProfiles.length})`, value: 'all' },
155+
{ name: 'Select specific profiles', value: 'select' },
156+
],
157+
default: 'all',
158+
});
159+
160+
if (exportAll === 'all') {
161+
selectedProfiles = allProfiles;
162+
} else {
163+
selectedProfiles = await interactiveCheckbox({
164+
message: 'Select profiles to export:',
165+
choices: allProfiles.map((p) => ({
166+
name: `${p.service}: ${p.profile}`,
167+
value: p,
168+
checked: false,
169+
})),
170+
required: true,
171+
});
172+
}
173+
}
174+
175+
// Filter config and credentials based on selection
176+
const filteredConfig: Config = { profiles: {} };
177+
const filteredCredentials: StoredCredentials = {};
178+
179+
for (const { service, profile } of selectedProfiles) {
180+
// Add to filtered config
181+
if (!filteredConfig.profiles[service]) {
182+
(filteredConfig.profiles as Record<string, string[]>)[service] = [];
183+
}
184+
(filteredConfig.profiles as Record<string, string[]>)[service].push(profile);
185+
186+
// Add credentials if they exist
187+
if (credentials[service]?.[profile]) {
188+
if (!filteredCredentials[service]) {
189+
filteredCredentials[service] = {};
190+
}
191+
filteredCredentials[service][profile] = credentials[service][profile];
192+
}
193+
}
194+
195+
// Include env vars if they exist
196+
if (configData.env) {
197+
filteredConfig.env = configData.env;
198+
}
199+
118200
const exportData: ExportedData = {
119201
version: 1,
120-
config: configData,
121-
credentials,
202+
config: filteredConfig,
203+
credentials: filteredCredentials,
122204
};
123205

124206
// Encrypt the data
125207
const key = deriveKeyFromPassword(encryptionKey);
126208
const encrypted = encrypt(JSON.stringify(exportData), key);
127209

210+
const profileCount = selectedProfiles.length;
211+
const profileText = profileCount === 1 ? 'profile' : 'profiles';
212+
128213
if (options.file) {
129214
// Write to file, output just the key
130215
const filePath = options.file.startsWith('/') ? options.file : join(process.cwd(), options.file);
131216
await writeFile(filePath, encrypted, { mode: 0o600 });
217+
console.error(`Exported ${profileCount} ${profileText} to ${filePath}`);
132218
console.log(`AGENTIO_KEY=${encryptionKey}`);
133219
} else {
134220
// Output as environment variables
221+
console.error(`Exported ${profileCount} ${profileText}`);
135222
console.log(`AGENTIO_KEY=${encryptionKey}`);
136223
console.log(`AGENTIO_CONFIG=${encrypted}`);
137224
}

0 commit comments

Comments
 (0)