@plosson/agentio 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/commands/config.ts +90 -3
- package/src/commands/docs.ts +126 -0
- package/src/commands/gchat.ts +12 -6
- package/src/commands/gdocs.ts +2 -1
- package/src/commands/gdrive.ts +6 -3
- package/src/commands/gmail.ts +10 -5
- package/src/commands/jira.ts +9 -14
- package/src/commands/sql.ts +11 -25
- package/src/index.ts +2 -0
- package/src/utils/interactive.ts +145 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plosson/agentio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "CLI for LLM agents to interact with communication and tracking services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"typescript": "^5.9.3"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"@inquirer/prompts": "^8.2.0",
|
|
49
50
|
"commander": "^14.0.2",
|
|
50
51
|
"googleapis": "^169.0.0",
|
|
51
52
|
"libsodium-wrappers": "^0.8.1",
|
package/src/commands/config.ts
CHANGED
|
@@ -7,9 +7,15 @@ import { loadConfig, saveConfig, setEnv, unsetEnv, listEnv } from '../config/con
|
|
|
7
7
|
import { getAllCredentials, setAllCredentials } from '../auth/token-store';
|
|
8
8
|
import { CliError, handleError } from '../utils/errors';
|
|
9
9
|
import { confirm } from '../utils/stdin';
|
|
10
|
-
import
|
|
10
|
+
import { isInteractive, interactiveCheckbox, interactiveSelect } from '../utils/interactive';
|
|
11
|
+
import type { Config, ServiceName } from '../types/config';
|
|
11
12
|
import type { StoredCredentials } from '../types/tokens';
|
|
12
13
|
|
|
14
|
+
interface ProfileSelection {
|
|
15
|
+
service: ServiceName;
|
|
16
|
+
profile: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
const ALGORITHM = 'aes-256-gcm';
|
|
14
20
|
|
|
15
21
|
interface ExportedData {
|
|
@@ -94,6 +100,7 @@ export function registerConfigCommands(program: Command): void {
|
|
|
94
100
|
.description('Export configuration and credentials (as environment variables by default, or to a file)')
|
|
95
101
|
.option('--key <key>', 'Encryption key (64 hex characters). If not provided, a random key will be generated')
|
|
96
102
|
.option('--file <path>', 'Write encrypted config to file instead of outputting AGENTIO_CONFIG')
|
|
103
|
+
.option('--all', 'Export all profiles without prompting for selection')
|
|
97
104
|
.action(async (options) => {
|
|
98
105
|
try {
|
|
99
106
|
// Validate key if provided
|
|
@@ -115,23 +122,103 @@ export function registerConfigCommands(program: Command): void {
|
|
|
115
122
|
const configData = await loadConfig();
|
|
116
123
|
const credentials = await getAllCredentials();
|
|
117
124
|
|
|
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
|
+
|
|
118
200
|
const exportData: ExportedData = {
|
|
119
201
|
version: 1,
|
|
120
|
-
config:
|
|
121
|
-
credentials,
|
|
202
|
+
config: filteredConfig,
|
|
203
|
+
credentials: filteredCredentials,
|
|
122
204
|
};
|
|
123
205
|
|
|
124
206
|
// Encrypt the data
|
|
125
207
|
const key = deriveKeyFromPassword(encryptionKey);
|
|
126
208
|
const encrypted = encrypt(JSON.stringify(exportData), key);
|
|
127
209
|
|
|
210
|
+
const profileCount = selectedProfiles.length;
|
|
211
|
+
const profileText = profileCount === 1 ? 'profile' : 'profiles';
|
|
212
|
+
|
|
128
213
|
if (options.file) {
|
|
129
214
|
// Write to file, output just the key
|
|
130
215
|
const filePath = options.file.startsWith('/') ? options.file : join(process.cwd(), options.file);
|
|
131
216
|
await writeFile(filePath, encrypted, { mode: 0o600 });
|
|
217
|
+
console.error(`Exported ${profileCount} ${profileText} to ${filePath}`);
|
|
132
218
|
console.log(`AGENTIO_KEY=${encryptionKey}`);
|
|
133
219
|
} else {
|
|
134
220
|
// Output as environment variables
|
|
221
|
+
console.error(`Exported ${profileCount} ${profileText}`);
|
|
135
222
|
console.log(`AGENTIO_KEY=${encryptionKey}`);
|
|
136
223
|
console.log(`AGENTIO_CONFIG=${encrypted}`);
|
|
137
224
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
interface CommandInfo {
|
|
4
|
+
fullPath: string;
|
|
5
|
+
description: string;
|
|
6
|
+
arguments: string[];
|
|
7
|
+
options: Array<{ flags: string; description: string; defaultValue?: string }>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function collectCommands(cmd: Command, parentPath: string = ''): CommandInfo[] {
|
|
11
|
+
const results: CommandInfo[] = [];
|
|
12
|
+
const help = cmd.createHelp();
|
|
13
|
+
|
|
14
|
+
// Get visible subcommands (filters out help command)
|
|
15
|
+
const subcommands = help.visibleCommands(cmd).filter((c) => c.name() !== 'help');
|
|
16
|
+
|
|
17
|
+
for (const subcmd of subcommands) {
|
|
18
|
+
const fullPath = parentPath ? `${parentPath} ${subcmd.name()}` : subcmd.name();
|
|
19
|
+
|
|
20
|
+
// Get arguments
|
|
21
|
+
const args = help.visibleArguments(subcmd).map((arg) => {
|
|
22
|
+
const argName = arg.variadic ? `${arg.name()}...` : arg.name();
|
|
23
|
+
return arg.required ? `<${argName}>` : `[${argName}]`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Get options (filter out help)
|
|
27
|
+
const options = help
|
|
28
|
+
.visibleOptions(subcmd)
|
|
29
|
+
.filter((opt) => !opt.long?.includes('help'))
|
|
30
|
+
.map((opt) => {
|
|
31
|
+
const flags = opt.flags;
|
|
32
|
+
const desc = opt.description;
|
|
33
|
+
const defaultVal = opt.defaultValue;
|
|
34
|
+
return { flags, description: desc, defaultValue: defaultVal };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const description = subcmd.description() || '';
|
|
38
|
+
|
|
39
|
+
// Only add if it's a leaf command or has its own action
|
|
40
|
+
const childCommands = help.visibleCommands(subcmd).filter((c) => c.name() !== 'help');
|
|
41
|
+
if (childCommands.length === 0 || options.length > 0 || args.length > 0) {
|
|
42
|
+
results.push({
|
|
43
|
+
fullPath,
|
|
44
|
+
description,
|
|
45
|
+
arguments: args,
|
|
46
|
+
options,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Recurse into subcommands
|
|
51
|
+
results.push(...collectCommands(subcmd, fullPath));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatOption(opt: { flags: string; description: string; defaultValue?: string }): string {
|
|
58
|
+
let line = opt.flags;
|
|
59
|
+
if (opt.description) {
|
|
60
|
+
line += `: ${opt.description}`;
|
|
61
|
+
}
|
|
62
|
+
if (opt.defaultValue !== undefined && opt.defaultValue !== '') {
|
|
63
|
+
line += ` (default: ${opt.defaultValue})`;
|
|
64
|
+
}
|
|
65
|
+
return line;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Commands excluded from docs output by default (utility/meta commands)
|
|
69
|
+
const EXCLUDED_COMMANDS = ['config', 'status', 'update', 'claude', 'docs'];
|
|
70
|
+
|
|
71
|
+
function generateDocs(program: Command, services?: string[]): string {
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
const version = program.version();
|
|
74
|
+
|
|
75
|
+
lines.push(`# agentio CLI v${version}`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
|
|
78
|
+
let commands = collectCommands(program, 'agentio');
|
|
79
|
+
|
|
80
|
+
// Filter by services if specified, otherwise exclude utility commands
|
|
81
|
+
// Always exclude profile subcommands
|
|
82
|
+
commands = commands.filter((cmd) => {
|
|
83
|
+
if (cmd.fullPath.includes(' profile ')) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const parts = cmd.fullPath.split(' ');
|
|
87
|
+
const service = parts[1];
|
|
88
|
+
if (services && services.length > 0) {
|
|
89
|
+
return services.includes(service);
|
|
90
|
+
}
|
|
91
|
+
return !EXCLUDED_COMMANDS.includes(service);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
for (const cmd of commands) {
|
|
95
|
+
// Header with full path and arguments
|
|
96
|
+
let header = `## ${cmd.fullPath}`;
|
|
97
|
+
if (cmd.arguments.length > 0) {
|
|
98
|
+
header += ` ${cmd.arguments.join(' ')}`;
|
|
99
|
+
}
|
|
100
|
+
lines.push(header);
|
|
101
|
+
|
|
102
|
+
// Description
|
|
103
|
+
if (cmd.description) {
|
|
104
|
+
lines.push(cmd.description);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Options
|
|
108
|
+
for (const opt of cmd.options) {
|
|
109
|
+
lines.push(formatOption(opt));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return lines.join('\n').trimEnd();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function registerDocsCommand(program: Command): void {
|
|
119
|
+
program
|
|
120
|
+
.command('docs')
|
|
121
|
+
.description('Output CLI reference for LLMs')
|
|
122
|
+
.option('--service <names>', 'Filter by service (comma-separated)', (val) => val.split(',').map((s: string) => s.trim()))
|
|
123
|
+
.action((options) => {
|
|
124
|
+
console.log(generateDocs(program, options.service));
|
|
125
|
+
});
|
|
126
|
+
}
|
package/src/commands/gchat.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { createGoogleAuth } from '../auth/token-manager';
|
|
|
10
10
|
import { GChatClient } from '../services/gchat/client';
|
|
11
11
|
import { CliError, handleError } from '../utils/errors';
|
|
12
12
|
import { readStdin, prompt } from '../utils/stdin';
|
|
13
|
+
import { interactiveSelect } from '../utils/interactive';
|
|
13
14
|
import { printGChatSendResult, printGChatMessageList, printGChatMessage, printGChatSpaceList } from '../utils/output';
|
|
14
15
|
import type { GChatCredentials, GChatWebhookCredentials, GChatOAuthCredentials } from '../types/gchat';
|
|
15
16
|
|
|
@@ -133,7 +134,8 @@ export function registerGChatCommands(program: Command): void {
|
|
|
133
134
|
});
|
|
134
135
|
|
|
135
136
|
gchat
|
|
136
|
-
.command('get
|
|
137
|
+
.command('get')
|
|
138
|
+
.argument('<message-id>', 'Message ID')
|
|
137
139
|
.description('Get a message from a Google Chat space (OAuth profiles only)')
|
|
138
140
|
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
139
141
|
.requiredOption('--space <id>', 'Space ID')
|
|
@@ -187,9 +189,15 @@ export function registerGChatCommands(program: Command): void {
|
|
|
187
189
|
try {
|
|
188
190
|
console.error('\nGoogle Chat Setup\n');
|
|
189
191
|
|
|
190
|
-
const profileType = await
|
|
192
|
+
const profileType = await interactiveSelect({
|
|
193
|
+
message: 'Choose profile type:',
|
|
194
|
+
choices: [
|
|
195
|
+
{ name: 'Webhook', value: 'webhook', description: 'Simple incoming webhook URL' },
|
|
196
|
+
{ name: 'OAuth', value: 'oauth', description: 'Full API access with Google Workspace account' },
|
|
197
|
+
],
|
|
198
|
+
});
|
|
191
199
|
|
|
192
|
-
if (profileType
|
|
200
|
+
if (profileType === 'webhook') {
|
|
193
201
|
if (!options.profile) {
|
|
194
202
|
throw new CliError(
|
|
195
203
|
'INVALID_PARAMS',
|
|
@@ -198,10 +206,8 @@ export function registerGChatCommands(program: Command): void {
|
|
|
198
206
|
);
|
|
199
207
|
}
|
|
200
208
|
await setupWebhookProfile(options.profile);
|
|
201
|
-
} else if (profileType.toLowerCase() === 'oauth') {
|
|
202
|
-
await setupOAuthProfile(options.profile);
|
|
203
209
|
} else {
|
|
204
|
-
|
|
210
|
+
await setupOAuthProfile(options.profile);
|
|
205
211
|
}
|
|
206
212
|
} catch (error) {
|
|
207
213
|
handleError(error);
|
package/src/commands/gdocs.ts
CHANGED
|
@@ -24,7 +24,8 @@ export function registerGDocsCommands(program: Command): void {
|
|
|
24
24
|
.description('Google Docs operations');
|
|
25
25
|
|
|
26
26
|
gdocs
|
|
27
|
-
.command('get
|
|
27
|
+
.command('get')
|
|
28
|
+
.argument('<doc-id-or-url>', 'Document ID or URL')
|
|
28
29
|
.description('Export a document')
|
|
29
30
|
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
30
31
|
.option('--format <format>', 'Export format: markdown or docx', 'markdown')
|
package/src/commands/gdrive.ts
CHANGED
|
@@ -95,7 +95,8 @@ Query Syntax Examples:
|
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
gdrive
|
|
98
|
-
.command('get
|
|
98
|
+
.command('get')
|
|
99
|
+
.argument('<file-id-or-url>', 'File ID or URL')
|
|
99
100
|
.description('Get file metadata')
|
|
100
101
|
.option('--profile <name>', 'Profile name')
|
|
101
102
|
.action(async (fileIdOrUrl: string, options) => {
|
|
@@ -132,7 +133,8 @@ Query Syntax Examples:
|
|
|
132
133
|
});
|
|
133
134
|
|
|
134
135
|
gdrive
|
|
135
|
-
.command('download
|
|
136
|
+
.command('download')
|
|
137
|
+
.argument('<file-id-or-url>', 'File ID or URL')
|
|
136
138
|
.description('Download a file (or export Google Workspace files)')
|
|
137
139
|
.option('--profile <name>', 'Profile name')
|
|
138
140
|
.requiredOption('--output <path>', 'Output file path')
|
|
@@ -164,7 +166,8 @@ Examples:
|
|
|
164
166
|
});
|
|
165
167
|
|
|
166
168
|
gdrive
|
|
167
|
-
.command('put
|
|
169
|
+
.command('put')
|
|
170
|
+
.argument('<file-path>', 'Local file path')
|
|
168
171
|
.description('Upload a file to Google Drive')
|
|
169
172
|
.option('--profile <name>', 'Profile name')
|
|
170
173
|
.option('--name <name>', 'Name for the file in Drive (defaults to local filename)')
|
package/src/commands/gmail.ts
CHANGED
|
@@ -101,7 +101,8 @@ export function registerGmailCommands(program: Command): void {
|
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
gmail
|
|
104
|
-
.command('get
|
|
104
|
+
.command('get')
|
|
105
|
+
.argument('<message-id>', 'Message ID')
|
|
105
106
|
.description('Get a message')
|
|
106
107
|
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
107
108
|
.option('--format <format>', 'Body format: text, html, or raw', 'text')
|
|
@@ -275,7 +276,8 @@ Query Syntax Examples:
|
|
|
275
276
|
});
|
|
276
277
|
|
|
277
278
|
gmail
|
|
278
|
-
.command('archive
|
|
279
|
+
.command('archive')
|
|
280
|
+
.argument('<message-id...>', 'Message ID(s)')
|
|
279
281
|
.description('Archive one or more messages')
|
|
280
282
|
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
281
283
|
.action(async (messageIds: string[], options) => {
|
|
@@ -291,7 +293,8 @@ Query Syntax Examples:
|
|
|
291
293
|
});
|
|
292
294
|
|
|
293
295
|
gmail
|
|
294
|
-
.command('mark
|
|
296
|
+
.command('mark')
|
|
297
|
+
.argument('<message-id...>', 'Message ID(s)')
|
|
295
298
|
.description('Mark one or more messages as read or unread')
|
|
296
299
|
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
297
300
|
.option('--read', 'Mark as read')
|
|
@@ -316,7 +319,8 @@ Query Syntax Examples:
|
|
|
316
319
|
});
|
|
317
320
|
|
|
318
321
|
gmail
|
|
319
|
-
.command('attachment
|
|
322
|
+
.command('attachment')
|
|
323
|
+
.argument('<message-id>', 'Message ID')
|
|
320
324
|
.description('Download attachments from a message')
|
|
321
325
|
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
322
326
|
.option('--name <filename>', 'Download specific attachment by filename (downloads all if not specified)')
|
|
@@ -359,7 +363,8 @@ Query Syntax Examples:
|
|
|
359
363
|
});
|
|
360
364
|
|
|
361
365
|
gmail
|
|
362
|
-
.command('export
|
|
366
|
+
.command('export')
|
|
367
|
+
.argument('<message-id>', 'Message ID')
|
|
363
368
|
.description('Export a message as PDF')
|
|
364
369
|
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
365
370
|
.option('--output <path>', 'Output file path', 'message.pdf')
|
package/src/commands/jira.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { createProfileCommands } from '../utils/profile-commands';
|
|
|
5
5
|
import { performJiraOAuthFlow, refreshJiraToken, type AtlassianSite } from '../auth/jira-oauth';
|
|
6
6
|
import { JiraClient } from '../services/jira/client';
|
|
7
7
|
import { CliError, handleError } from '../utils/errors';
|
|
8
|
-
import { readStdin
|
|
8
|
+
import { readStdin } from '../utils/stdin';
|
|
9
|
+
import { interactiveSelect } from '../utils/interactive';
|
|
9
10
|
import {
|
|
10
11
|
printJiraProjectList,
|
|
11
12
|
printJiraIssueList,
|
|
@@ -220,20 +221,14 @@ export function registerJiraCommands(program: Command): void {
|
|
|
220
221
|
|
|
221
222
|
// Site selection callback
|
|
222
223
|
const selectSite = async (sites: AtlassianSite[]): Promise<AtlassianSite> => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
return interactiveSelect({
|
|
225
|
+
message: 'Select a JIRA site:',
|
|
226
|
+
choices: sites.map((site) => ({
|
|
227
|
+
name: site.name,
|
|
228
|
+
value: site,
|
|
229
|
+
description: site.url,
|
|
230
|
+
})),
|
|
226
231
|
});
|
|
227
|
-
console.error('');
|
|
228
|
-
|
|
229
|
-
const choice = await prompt(`? Select a site (1-${sites.length}): `);
|
|
230
|
-
const index = parseInt(choice, 10) - 1;
|
|
231
|
-
|
|
232
|
-
if (isNaN(index) || index < 0 || index >= sites.length) {
|
|
233
|
-
throw new CliError('INVALID_PARAMS', 'Invalid selection');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return sites[index];
|
|
237
232
|
};
|
|
238
233
|
|
|
239
234
|
const result = await performJiraOAuthFlow(selectSite);
|
package/src/commands/sql.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createClientGetter } from '../utils/client-factory';
|
|
|
6
6
|
import { SqlClient } from '../services/sql/client';
|
|
7
7
|
import { CliError, handleError } from '../utils/errors';
|
|
8
8
|
import { readStdin, prompt } from '../utils/stdin';
|
|
9
|
+
import { interactiveSelect } from '../utils/interactive';
|
|
9
10
|
import type { SqlCredentials } from '../types/sql';
|
|
10
11
|
|
|
11
12
|
const getSqlClient = createClientGetter<SqlCredentials, SqlClient>({
|
|
@@ -138,31 +139,16 @@ async function promptInteractiveConnection(): Promise<string> {
|
|
|
138
139
|
console.error('\nSQL Database Setup (Interactive)\n');
|
|
139
140
|
|
|
140
141
|
// Database type
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
case '1':
|
|
152
|
-
dbType = 'postgres';
|
|
153
|
-
defaultPort = '5432';
|
|
154
|
-
break;
|
|
155
|
-
case '2':
|
|
156
|
-
dbType = 'mysql';
|
|
157
|
-
defaultPort = '3306';
|
|
158
|
-
break;
|
|
159
|
-
case '3':
|
|
160
|
-
dbType = 'sqlite';
|
|
161
|
-
defaultPort = '';
|
|
162
|
-
break;
|
|
163
|
-
default:
|
|
164
|
-
throw new CliError('INVALID_PARAMS', 'Invalid database type selection');
|
|
165
|
-
}
|
|
142
|
+
const dbType = await interactiveSelect({
|
|
143
|
+
message: 'Select database type:',
|
|
144
|
+
choices: [
|
|
145
|
+
{ name: 'PostgreSQL', value: 'postgres' as const, description: 'Default port 5432' },
|
|
146
|
+
{ name: 'MySQL', value: 'mysql' as const, description: 'Default port 3306' },
|
|
147
|
+
{ name: 'SQLite', value: 'sqlite' as const, description: 'Local file database' },
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const defaultPort = dbType === 'postgres' ? '5432' : dbType === 'mysql' ? '3306' : '';
|
|
166
152
|
|
|
167
153
|
// SQLite only needs a file path
|
|
168
154
|
if (dbType === 'sqlite') {
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { registerUpdateCommand } from './commands/update';
|
|
|
15
15
|
import { registerConfigCommands } from './commands/config';
|
|
16
16
|
import { registerClaudeCommands } from './commands/claude';
|
|
17
17
|
import { registerStatusCommand } from './commands/status';
|
|
18
|
+
import { registerDocsCommand } from './commands/docs';
|
|
18
19
|
|
|
19
20
|
declare const BUILD_VERSION: string | undefined;
|
|
20
21
|
|
|
@@ -48,5 +49,6 @@ registerUpdateCommand(program);
|
|
|
48
49
|
registerConfigCommands(program);
|
|
49
50
|
registerClaudeCommands(program);
|
|
50
51
|
registerStatusCommand(program);
|
|
52
|
+
registerDocsCommand(program);
|
|
51
53
|
|
|
52
54
|
program.parse();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
select,
|
|
3
|
+
checkbox,
|
|
4
|
+
confirm as inquirerConfirm,
|
|
5
|
+
input,
|
|
6
|
+
} from '@inquirer/prompts';
|
|
7
|
+
import { CliError } from './errors';
|
|
8
|
+
|
|
9
|
+
export interface SelectChoice<T> {
|
|
10
|
+
name: string;
|
|
11
|
+
value: T;
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CheckboxChoice<T> {
|
|
16
|
+
name: string;
|
|
17
|
+
value: T;
|
|
18
|
+
checked?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if running in an interactive terminal.
|
|
23
|
+
*/
|
|
24
|
+
export function isInteractive(): boolean {
|
|
25
|
+
return process.stdin.isTTY === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Interactive select prompt. Falls back to default or throws if not in TTY.
|
|
30
|
+
*/
|
|
31
|
+
export async function interactiveSelect<T>(options: {
|
|
32
|
+
message: string;
|
|
33
|
+
choices: SelectChoice<T>[];
|
|
34
|
+
default?: T;
|
|
35
|
+
}): Promise<T> {
|
|
36
|
+
if (!isInteractive()) {
|
|
37
|
+
if (options.default !== undefined) {
|
|
38
|
+
return options.default;
|
|
39
|
+
}
|
|
40
|
+
throw new CliError(
|
|
41
|
+
'INVALID_PARAMS',
|
|
42
|
+
'Interactive input required but not running in terminal',
|
|
43
|
+
'Run this command in an interactive terminal'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return select({
|
|
48
|
+
message: options.message,
|
|
49
|
+
choices: options.choices,
|
|
50
|
+
default: options.default,
|
|
51
|
+
loop: false,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Interactive checkbox (multi-select) prompt. Falls back to default or throws if not in TTY.
|
|
57
|
+
*/
|
|
58
|
+
export async function interactiveCheckbox<T>(options: {
|
|
59
|
+
message: string;
|
|
60
|
+
choices: CheckboxChoice<T>[];
|
|
61
|
+
required?: boolean;
|
|
62
|
+
}): Promise<T[]> {
|
|
63
|
+
if (!isInteractive()) {
|
|
64
|
+
// Return all checked items as default
|
|
65
|
+
const defaults = options.choices.filter((c) => c.checked).map((c) => c.value);
|
|
66
|
+
if (defaults.length > 0 || !options.required) {
|
|
67
|
+
return defaults;
|
|
68
|
+
}
|
|
69
|
+
throw new CliError(
|
|
70
|
+
'INVALID_PARAMS',
|
|
71
|
+
'Interactive input required but not running in terminal',
|
|
72
|
+
'Run this command in an interactive terminal'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = await checkbox({
|
|
77
|
+
message: options.message,
|
|
78
|
+
choices: options.choices,
|
|
79
|
+
loop: false,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (options.required && result.length === 0) {
|
|
83
|
+
throw new CliError('INVALID_PARAMS', 'At least one option must be selected');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Interactive confirm prompt. Falls back to default or throws if not in TTY.
|
|
91
|
+
*/
|
|
92
|
+
export async function interactiveConfirm(options: {
|
|
93
|
+
message: string;
|
|
94
|
+
default?: boolean;
|
|
95
|
+
}): Promise<boolean> {
|
|
96
|
+
if (!isInteractive()) {
|
|
97
|
+
if (options.default !== undefined) {
|
|
98
|
+
return options.default;
|
|
99
|
+
}
|
|
100
|
+
throw new CliError(
|
|
101
|
+
'INVALID_PARAMS',
|
|
102
|
+
'Interactive input required but not running in terminal',
|
|
103
|
+
'Run this command in an interactive terminal'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return inquirerConfirm({
|
|
108
|
+
message: options.message,
|
|
109
|
+
default: options.default,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Interactive text input prompt. Falls back to default or throws if not in TTY.
|
|
115
|
+
*/
|
|
116
|
+
export async function interactiveInput(options: {
|
|
117
|
+
message: string;
|
|
118
|
+
default?: string;
|
|
119
|
+
required?: boolean;
|
|
120
|
+
}): Promise<string> {
|
|
121
|
+
if (!isInteractive()) {
|
|
122
|
+
if (options.default !== undefined) {
|
|
123
|
+
return options.default;
|
|
124
|
+
}
|
|
125
|
+
if (!options.required) {
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
throw new CliError(
|
|
129
|
+
'INVALID_PARAMS',
|
|
130
|
+
'Interactive input required but not running in terminal',
|
|
131
|
+
'Run this command in an interactive terminal'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = await input({
|
|
136
|
+
message: options.message,
|
|
137
|
+
default: options.default,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (options.required && !result.trim()) {
|
|
141
|
+
throw new CliError('INVALID_PARAMS', 'Input is required');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
145
|
+
}
|