@ktmcp-cli/awsathena 1.0.0

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/AGENT.md ADDED
@@ -0,0 +1,93 @@
1
+ # AGENT.md — Amazon Athena CLI for AI Agents
2
+
3
+ This document explains how to use the Amazon Athena CLI as an AI agent.
4
+
5
+ ## Overview
6
+
7
+ The `awsathena` CLI provides access to the Amazon Athena Query API. Requires AWS credentials with Athena permissions.
8
+
9
+ ## Prerequisites
10
+
11
+ ```bash
12
+ awsathena config set accessKeyId YOUR_AWS_ACCESS_KEY_ID
13
+ awsathena config set secretAccessKey YOUR_AWS_SECRET_ACCESS_KEY
14
+ awsathena config set region us-east-1
15
+ ```
16
+
17
+ ## All Commands
18
+
19
+ ### Config
20
+
21
+ ```bash
22
+ awsathena config get <key>
23
+ awsathena config set <key> <value>
24
+ awsathena config list
25
+ ```
26
+
27
+ ### Queries
28
+
29
+ ```bash
30
+ # Run query
31
+ awsathena queries run --sql "SELECT * FROM table LIMIT 10" --database my_db --output s3://bucket/results/
32
+ awsathena queries run --sql "SELECT count(*) FROM table" --workgroup primary
33
+
34
+ # Check status
35
+ awsathena queries get <execution-id>
36
+
37
+ # List recent queries
38
+ awsathena queries list
39
+ awsathena queries list --workgroup primary
40
+
41
+ # Stop query
42
+ awsathena queries stop <execution-id>
43
+
44
+ # Get results
45
+ awsathena queries results <execution-id>
46
+ awsathena queries results <execution-id> --limit 100
47
+ ```
48
+
49
+ ### Databases
50
+
51
+ ```bash
52
+ awsathena databases list
53
+ awsathena databases list --catalog AwsDataCatalog
54
+ awsathena databases get <database-name>
55
+ awsathena databases create <database-name> --output s3://bucket/results/
56
+ ```
57
+
58
+ ### Workgroups
59
+
60
+ ```bash
61
+ awsathena workgroups list
62
+ awsathena workgroups get <workgroup-name>
63
+ awsathena workgroups create <name> --description "desc" --output s3://bucket/results/
64
+ ```
65
+
66
+ ## JSON Output
67
+
68
+ All commands support `--json`:
69
+
70
+ ```bash
71
+ awsathena queries get <id> --json
72
+ awsathena databases list --json
73
+ awsathena workgroups list --json
74
+ ```
75
+
76
+ ## Typical Workflow
77
+
78
+ ```bash
79
+ # 1. Start a query
80
+ awsathena queries run --sql "SELECT * FROM my_db.my_table LIMIT 100" --output s3://my-bucket/results/
81
+
82
+ # 2. Check status (wait for SUCCEEDED)
83
+ awsathena queries get <execution-id>
84
+
85
+ # 3. Get results
86
+ awsathena queries results <execution-id> --json
87
+ ```
88
+
89
+ ## Error Handling
90
+
91
+ The CLI exits with code 1 on error and prints to stderr.
92
+ - `AWS authentication failed` — Check accessKeyId and secretAccessKey
93
+ - `Resource not found` — Check query execution ID or resource name
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KTMCP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ > "Six months ago, everyone was talking about MCPs. And I was like, screw MCPs. Every MCP would be better as a CLI."
2
+ >
3
+ > — [Peter Steinberger](https://twitter.com/steipete), Founder of OpenClaw
4
+ > [Watch on YouTube (~2:39:00)](https://www.youtube.com/@lexfridman) | [Lex Fridman Podcast #491](https://lexfridman.com/peter-steinberger/)
5
+
6
+ # Amazon Athena CLI
7
+
8
+ Production-ready CLI for the Amazon Athena Query API. Run SQL queries and manage databases and workgroups directly from your terminal.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -g @ktmcp-cli/awsathena
14
+ ```
15
+
16
+ ## Configuration
17
+
18
+ ```bash
19
+ awsathena config set accessKeyId YOUR_AWS_ACCESS_KEY_ID
20
+ awsathena config set secretAccessKey YOUR_AWS_SECRET_ACCESS_KEY
21
+ awsathena config set region us-east-1
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Queries
27
+
28
+ ```bash
29
+ # Run a SQL query
30
+ awsathena queries run --sql "SELECT * FROM my_table LIMIT 10" --database my_db --output s3://my-bucket/results/
31
+
32
+ # Check query status
33
+ awsathena queries get <execution-id>
34
+
35
+ # List recent queries
36
+ awsathena queries list
37
+ awsathena queries list --workgroup primary
38
+
39
+ # Stop a running query
40
+ awsathena queries stop <execution-id>
41
+
42
+ # Get query results
43
+ awsathena queries results <execution-id>
44
+ awsathena queries results <execution-id> --limit 100
45
+ ```
46
+
47
+ ### Databases
48
+
49
+ ```bash
50
+ # List all databases
51
+ awsathena databases list
52
+ awsathena databases list --catalog AwsDataCatalog
53
+
54
+ # Get database details
55
+ awsathena databases get my_database
56
+
57
+ # Create a database
58
+ awsathena databases create new_database --output s3://my-bucket/results/
59
+ ```
60
+
61
+ ### Workgroups
62
+
63
+ ```bash
64
+ # List workgroups
65
+ awsathena workgroups list
66
+
67
+ # Get workgroup details
68
+ awsathena workgroups get primary
69
+
70
+ # Create a workgroup
71
+ awsathena workgroups create my-team --description "My team workgroup" --output s3://my-bucket/results/
72
+ ```
73
+
74
+ ### JSON Output
75
+
76
+ All commands support `--json`:
77
+
78
+ ```bash
79
+ awsathena queries get <id> --json
80
+ awsathena databases list --json | jq '.[].Name'
81
+ awsathena queries results <id> --json
82
+ ```
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ import(join(__dirname, '..', 'src', 'index.js'));
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ktmcp-cli/awsathena",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI for Amazon Athena Query API - Kill The MCP",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "awsathena": "bin/awsathena.js"
9
+ },
10
+ "keywords": ["awsathena", "athena", "aws", "sql", "cli", "api", "ktmcp"],
11
+ "author": "KTMCP",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "commander": "^12.0.0",
15
+ "axios": "^1.6.7",
16
+ "chalk": "^5.3.0",
17
+ "ora": "^8.0.1",
18
+ "conf": "^12.0.0"
19
+ },
20
+ "engines": { "node": ">=18.0.0" },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ktmcp-cli/awsathena.git"
24
+ },
25
+ "homepage": "https://killthemcp.com/awsathena-cli",
26
+ "bugs": { "url": "https://github.com/ktmcp-cli/awsathena/issues" }
27
+ }
package/src/api.js ADDED
@@ -0,0 +1,206 @@
1
+ import axios from 'axios';
2
+ import crypto from 'crypto';
3
+ import { getConfig } from './config.js';
4
+
5
+ /**
6
+ * AWS Signature Version 4 signing helper
7
+ */
8
+ function sign(key, msg) {
9
+ return crypto.createHmac('sha256', key).update(msg).digest();
10
+ }
11
+
12
+ function getSigningKey(secretKey, dateStamp, regionName, serviceName) {
13
+ const kDate = sign('AWS4' + secretKey, dateStamp);
14
+ const kRegion = sign(kDate, regionName);
15
+ const kService = sign(kRegion, serviceName);
16
+ return sign(kService, 'aws4_request');
17
+ }
18
+
19
+ function buildAuthHeader({ method, url, body, service, region, accessKeyId, secretAccessKey, target }) {
20
+ const parsedUrl = new URL(url);
21
+ const host = parsedUrl.host;
22
+ const path = parsedUrl.pathname;
23
+ const queryString = parsedUrl.search ? parsedUrl.search.slice(1) : '';
24
+
25
+ const now = new Date();
26
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '').slice(0, 15) + 'Z';
27
+ const dateStamp = amzDate.slice(0, 8);
28
+
29
+ const payloadHash = crypto.createHash('sha256').update(body || '').digest('hex');
30
+
31
+ const canonicalHeaders = `content-type:application/x-amz-json-1.1\nhost:${host}\nx-amz-date:${amzDate}\nx-amz-target:${target}\n`;
32
+ const signedHeaders = 'content-type;host;x-amz-date;x-amz-target';
33
+
34
+ const canonicalRequest = [
35
+ method.toUpperCase(),
36
+ path,
37
+ queryString,
38
+ canonicalHeaders,
39
+ signedHeaders,
40
+ payloadHash
41
+ ].join('\n');
42
+
43
+ const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
44
+ const stringToSign = [
45
+ 'AWS4-HMAC-SHA256',
46
+ amzDate,
47
+ credentialScope,
48
+ crypto.createHash('sha256').update(canonicalRequest).digest('hex')
49
+ ].join('\n');
50
+
51
+ const signingKey = getSigningKey(secretAccessKey, dateStamp, region, service);
52
+ const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');
53
+
54
+ const authorization = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
55
+
56
+ return { authorization, amzDate };
57
+ }
58
+
59
+ async function athenaRequest(action, body = {}) {
60
+ const accessKeyId = getConfig('accessKeyId');
61
+ const secretAccessKey = getConfig('secretAccessKey');
62
+ const region = getConfig('region') || 'us-east-1';
63
+ const url = `https://athena.${region}.amazonaws.com/`;
64
+ const target = `AmazonAthena.${action}`;
65
+ const bodyStr = JSON.stringify(body);
66
+
67
+ const { authorization, amzDate } = buildAuthHeader({
68
+ method: 'POST',
69
+ url,
70
+ body: bodyStr,
71
+ service: 'athena',
72
+ region,
73
+ accessKeyId,
74
+ secretAccessKey,
75
+ target
76
+ });
77
+
78
+ try {
79
+ const response = await axios.post(url, bodyStr, {
80
+ headers: {
81
+ 'Authorization': authorization,
82
+ 'X-Amz-Date': amzDate,
83
+ 'X-Amz-Target': target,
84
+ 'Content-Type': 'application/x-amz-json-1.1',
85
+ 'Accept': 'application/json'
86
+ }
87
+ });
88
+ return response.data;
89
+ } catch (error) {
90
+ handleApiError(error);
91
+ }
92
+ }
93
+
94
+ function handleApiError(error) {
95
+ if (error.response) {
96
+ const status = error.response.status;
97
+ const data = error.response.data;
98
+ if (status === 401 || status === 403) {
99
+ throw new Error('AWS authentication failed. Check your accessKeyId and secretAccessKey.');
100
+ } else if (status === 404) {
101
+ throw new Error('Resource not found.');
102
+ } else if (status === 429) {
103
+ throw new Error('Rate limit exceeded. Please wait before retrying.');
104
+ } else {
105
+ const message = data?.message || data?.Message || data?.__type || JSON.stringify(data);
106
+ throw new Error(`AWS Athena Error (${status}): ${message}`);
107
+ }
108
+ } else if (error.request) {
109
+ throw new Error('No response from AWS Athena API. Check your internet connection and region.');
110
+ } else {
111
+ throw error;
112
+ }
113
+ }
114
+
115
+ // ============================================================
116
+ // QUERIES
117
+ // ============================================================
118
+
119
+ export async function startQuery({ sql, database, workgroup, outputLocation }) {
120
+ const body = {
121
+ QueryString: sql,
122
+ ...(database && { QueryExecutionContext: { Database: database } }),
123
+ ...(workgroup && { WorkGroup: workgroup }),
124
+ ...(outputLocation && { ResultConfiguration: { OutputLocation: outputLocation } })
125
+ };
126
+ const data = await athenaRequest('StartQueryExecution', body);
127
+ return data;
128
+ }
129
+
130
+ export async function getQuery(queryExecutionId) {
131
+ const data = await athenaRequest('GetQueryExecution', { QueryExecutionId: queryExecutionId });
132
+ return data?.QueryExecution || null;
133
+ }
134
+
135
+ export async function listQueries(workgroup) {
136
+ const body = {};
137
+ if (workgroup) body.WorkGroup = workgroup;
138
+ const data = await athenaRequest('ListQueryExecutions', body);
139
+ return data?.QueryExecutionIds || [];
140
+ }
141
+
142
+ export async function stopQuery(queryExecutionId) {
143
+ await athenaRequest('StopQueryExecution', { QueryExecutionId: queryExecutionId });
144
+ return { stopped: true };
145
+ }
146
+
147
+ export async function getQueryResults(queryExecutionId, maxResults = 20) {
148
+ const data = await athenaRequest('GetQueryResults', {
149
+ QueryExecutionId: queryExecutionId,
150
+ MaxResults: maxResults
151
+ });
152
+ return data;
153
+ }
154
+
155
+ // ============================================================
156
+ // DATABASES
157
+ // ============================================================
158
+
159
+ export async function listDatabases(catalogName = 'AwsDataCatalog') {
160
+ const data = await athenaRequest('ListDatabases', { CatalogName: catalogName });
161
+ return data?.DatabaseList || [];
162
+ }
163
+
164
+ export async function getDatabase(catalogName, databaseName) {
165
+ const data = await athenaRequest('GetDatabase', {
166
+ CatalogName: catalogName || 'AwsDataCatalog',
167
+ DatabaseName: databaseName
168
+ });
169
+ return data?.Database || null;
170
+ }
171
+
172
+ export async function createDatabase({ catalogName, databaseName, description, outputLocation }) {
173
+ // Athena creates databases via DDL query
174
+ const sql = description
175
+ ? `CREATE DATABASE IF NOT EXISTS ${databaseName} COMMENT '${description}'`
176
+ : `CREATE DATABASE IF NOT EXISTS ${databaseName}`;
177
+ return await startQuery({ sql, outputLocation });
178
+ }
179
+
180
+ // ============================================================
181
+ // WORKGROUPS
182
+ // ============================================================
183
+
184
+ export async function listWorkgroups() {
185
+ const data = await athenaRequest('ListWorkGroups', {});
186
+ return data?.WorkGroups || [];
187
+ }
188
+
189
+ export async function getWorkgroup(workgroupName) {
190
+ const data = await athenaRequest('GetWorkGroup', { WorkGroup: workgroupName });
191
+ return data?.WorkGroup || null;
192
+ }
193
+
194
+ export async function createWorkgroup({ name, description, outputLocation }) {
195
+ const body = {
196
+ Name: name,
197
+ ...(description && { Description: description }),
198
+ ...(outputLocation && {
199
+ Configuration: {
200
+ ResultConfiguration: { OutputLocation: outputLocation }
201
+ }
202
+ })
203
+ };
204
+ const data = await athenaRequest('CreateWorkGroup', body);
205
+ return data || { created: true };
206
+ }
package/src/config.js ADDED
@@ -0,0 +1,21 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({ projectName: '@ktmcp-cli/awsathena' });
4
+
5
+ export function getConfig(key) {
6
+ return config.get(key);
7
+ }
8
+
9
+ export function setConfig(key, value) {
10
+ config.set(key, value);
11
+ }
12
+
13
+ export function isConfigured() {
14
+ return !!(config.get('accessKeyId') && config.get('secretAccessKey'));
15
+ }
16
+
17
+ export function getAllConfig() {
18
+ return config.store;
19
+ }
20
+
21
+ export default config;
package/src/index.js ADDED
@@ -0,0 +1,523 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getConfig, setConfig, getAllConfig, isConfigured } from './config.js';
5
+ import {
6
+ startQuery,
7
+ getQuery,
8
+ listQueries,
9
+ stopQuery,
10
+ getQueryResults,
11
+ listDatabases,
12
+ getDatabase,
13
+ createDatabase,
14
+ listWorkgroups,
15
+ getWorkgroup,
16
+ createWorkgroup
17
+ } from './api.js';
18
+
19
+ const program = new Command();
20
+
21
+ // ============================================================
22
+ // Helpers
23
+ // ============================================================
24
+
25
+ function printSuccess(message) {
26
+ console.log(chalk.green('✓') + ' ' + message);
27
+ }
28
+
29
+ function printError(message) {
30
+ console.error(chalk.red('✗') + ' ' + message);
31
+ }
32
+
33
+ function printTable(data, columns) {
34
+ if (!data || data.length === 0) {
35
+ console.log(chalk.yellow('No results found.'));
36
+ return;
37
+ }
38
+
39
+ const widths = {};
40
+ columns.forEach(col => {
41
+ widths[col.key] = col.label.length;
42
+ data.forEach(row => {
43
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
44
+ if (val.length > widths[col.key]) widths[col.key] = val.length;
45
+ });
46
+ widths[col.key] = Math.min(widths[col.key], 40);
47
+ });
48
+
49
+ const header = columns.map(col => col.label.padEnd(widths[col.key])).join(' ');
50
+ console.log(chalk.bold(chalk.cyan(header)));
51
+ console.log(chalk.dim('─'.repeat(header.length)));
52
+
53
+ data.forEach(row => {
54
+ const line = columns.map(col => {
55
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
56
+ return val.substring(0, widths[col.key]).padEnd(widths[col.key]);
57
+ }).join(' ');
58
+ console.log(line);
59
+ });
60
+
61
+ console.log(chalk.dim(`\n${data.length} result(s)`));
62
+ }
63
+
64
+ function printJson(data) {
65
+ console.log(JSON.stringify(data, null, 2));
66
+ }
67
+
68
+ async function withSpinner(message, fn) {
69
+ const spinner = ora(message).start();
70
+ try {
71
+ const result = await fn();
72
+ spinner.stop();
73
+ return result;
74
+ } catch (error) {
75
+ spinner.stop();
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ function requireAuth() {
81
+ if (!isConfigured()) {
82
+ printError('AWS credentials not configured.');
83
+ console.log('\nRun the following to configure:');
84
+ console.log(chalk.cyan(' awsathena config set accessKeyId YOUR_KEY'));
85
+ console.log(chalk.cyan(' awsathena config set secretAccessKey YOUR_SECRET'));
86
+ console.log(chalk.cyan(' awsathena config set region us-east-1'));
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ // ============================================================
92
+ // Program metadata
93
+ // ============================================================
94
+
95
+ program
96
+ .name('awsathena')
97
+ .description(chalk.bold('Amazon Athena CLI') + ' - Run queries and manage databases from your terminal')
98
+ .version('1.0.0');
99
+
100
+ // ============================================================
101
+ // CONFIG
102
+ // ============================================================
103
+
104
+ const configCmd = program.command('config').description('Manage CLI configuration');
105
+
106
+ configCmd
107
+ .command('get <key>')
108
+ .description('Get a configuration value')
109
+ .action((key) => {
110
+ const value = getConfig(key);
111
+ if (value === undefined) {
112
+ printError(`Key '${key}' not found`);
113
+ } else {
114
+ console.log(value);
115
+ }
116
+ });
117
+
118
+ configCmd
119
+ .command('set <key> <value>')
120
+ .description('Set a configuration value')
121
+ .action((key, value) => {
122
+ setConfig(key, value);
123
+ printSuccess(`Config '${key}' set`);
124
+ });
125
+
126
+ configCmd
127
+ .command('list')
128
+ .description('List all configuration values')
129
+ .action(() => {
130
+ const all = getAllConfig();
131
+ console.log(chalk.bold('\nAmazon Athena CLI Configuration\n'));
132
+ if (Object.keys(all).length === 0) {
133
+ console.log(chalk.yellow('No configuration set.'));
134
+ console.log('\nRun:');
135
+ console.log(chalk.cyan(' awsathena config set accessKeyId YOUR_KEY'));
136
+ console.log(chalk.cyan(' awsathena config set secretAccessKey YOUR_SECRET'));
137
+ console.log(chalk.cyan(' awsathena config set region us-east-1'));
138
+ } else {
139
+ Object.entries(all).forEach(([k, v]) => {
140
+ const displayVal = k === 'secretAccessKey' ? chalk.green('*'.repeat(8)) : chalk.cyan(String(v));
141
+ console.log(`${k}: ${displayVal}`);
142
+ });
143
+ }
144
+ });
145
+
146
+ // ============================================================
147
+ // QUERIES
148
+ // ============================================================
149
+
150
+ const queriesCmd = program.command('queries').description('Run and manage Athena queries');
151
+
152
+ queriesCmd
153
+ .command('run')
154
+ .description('Run a SQL query')
155
+ .requiredOption('--sql <sql>', 'SQL query to execute')
156
+ .option('--database <db>', 'Database to query')
157
+ .option('--workgroup <wg>', 'Workgroup to use')
158
+ .option('--output <s3-path>', 'S3 output location (e.g. s3://bucket/prefix/)')
159
+ .option('--json', 'Output as JSON')
160
+ .action(async (options) => {
161
+ requireAuth();
162
+ try {
163
+ const result = await withSpinner('Starting query...', () =>
164
+ startQuery({
165
+ sql: options.sql,
166
+ database: options.database,
167
+ workgroup: options.workgroup,
168
+ outputLocation: options.output
169
+ })
170
+ );
171
+
172
+ if (options.json) {
173
+ printJson(result);
174
+ return;
175
+ }
176
+
177
+ printSuccess('Query started');
178
+ console.log('Execution ID: ', chalk.cyan(result?.QueryExecutionId || JSON.stringify(result)));
179
+ console.log('\nUse this ID to check status or get results:');
180
+ console.log(chalk.dim(` awsathena queries get ${result?.QueryExecutionId}`));
181
+ console.log(chalk.dim(` awsathena queries results ${result?.QueryExecutionId}`));
182
+ } catch (error) {
183
+ printError(error.message);
184
+ process.exit(1);
185
+ }
186
+ });
187
+
188
+ queriesCmd
189
+ .command('get <execution-id>')
190
+ .description('Get query execution status')
191
+ .option('--json', 'Output as JSON')
192
+ .action(async (executionId, options) => {
193
+ requireAuth();
194
+ try {
195
+ const query = await withSpinner('Fetching query status...', () => getQuery(executionId));
196
+
197
+ if (!query) {
198
+ printError('Query execution not found');
199
+ process.exit(1);
200
+ }
201
+
202
+ if (options.json) {
203
+ printJson(query);
204
+ return;
205
+ }
206
+
207
+ const state = query.Status?.State;
208
+ const stateColor = state === 'SUCCEEDED' ? chalk.green : state === 'FAILED' ? chalk.red : chalk.yellow;
209
+
210
+ console.log(chalk.bold('\nQuery Execution Details\n'));
211
+ console.log('Execution ID: ', chalk.cyan(query.QueryExecutionId));
212
+ console.log('Status: ', stateColor(state || 'N/A'));
213
+ console.log('Query: ', (query.Query || '').substring(0, 80));
214
+ console.log('Database: ', query.QueryExecutionContext?.Database || 'N/A');
215
+ console.log('Workgroup: ', query.WorkGroup || 'N/A');
216
+ console.log('Output: ', query.ResultConfiguration?.OutputLocation || 'N/A');
217
+ if (query.Status?.StateChangeReason) {
218
+ console.log('Reason: ', chalk.red(query.Status.StateChangeReason));
219
+ }
220
+ if (query.Statistics) {
221
+ console.log('Data Scanned: ', query.Statistics.DataScannedInBytes ? `${(query.Statistics.DataScannedInBytes / 1024 / 1024).toFixed(2)} MB` : 'N/A');
222
+ console.log('Exec Time: ', query.Statistics.TotalExecutionTimeInMillis ? `${query.Statistics.TotalExecutionTimeInMillis}ms` : 'N/A');
223
+ }
224
+ console.log('');
225
+ } catch (error) {
226
+ printError(error.message);
227
+ process.exit(1);
228
+ }
229
+ });
230
+
231
+ queriesCmd
232
+ .command('list')
233
+ .description('List recent query executions')
234
+ .option('--workgroup <wg>', 'Filter by workgroup')
235
+ .option('--json', 'Output as JSON')
236
+ .action(async (options) => {
237
+ requireAuth();
238
+ try {
239
+ const ids = await withSpinner('Fetching query list...', () => listQueries(options.workgroup));
240
+
241
+ if (options.json) {
242
+ printJson(ids);
243
+ return;
244
+ }
245
+
246
+ if (!ids || ids.length === 0) {
247
+ console.log(chalk.yellow('No query executions found.'));
248
+ return;
249
+ }
250
+
251
+ console.log(chalk.bold('\nRecent Query Executions\n'));
252
+ ids.forEach((id, i) => {
253
+ console.log(`${chalk.dim(String(i + 1).padStart(3, ' '))}. ${chalk.cyan(id)}`);
254
+ });
255
+ console.log(chalk.dim(`\n${ids.length} execution(s)`));
256
+ } catch (error) {
257
+ printError(error.message);
258
+ process.exit(1);
259
+ }
260
+ });
261
+
262
+ queriesCmd
263
+ .command('stop <execution-id>')
264
+ .description('Stop a running query')
265
+ .action(async (executionId) => {
266
+ requireAuth();
267
+ try {
268
+ await withSpinner(`Stopping query ${executionId}...`, () => stopQuery(executionId));
269
+ printSuccess(`Query ${executionId} stopped`);
270
+ } catch (error) {
271
+ printError(error.message);
272
+ process.exit(1);
273
+ }
274
+ });
275
+
276
+ queriesCmd
277
+ .command('results <execution-id>')
278
+ .description('Get query results')
279
+ .option('--limit <n>', 'Maximum number of rows', '20')
280
+ .option('--json', 'Output as JSON')
281
+ .action(async (executionId, options) => {
282
+ requireAuth();
283
+ try {
284
+ const results = await withSpinner('Fetching query results...', () =>
285
+ getQueryResults(executionId, parseInt(options.limit))
286
+ );
287
+
288
+ if (options.json) {
289
+ printJson(results);
290
+ return;
291
+ }
292
+
293
+ if (!results?.ResultSet?.Rows || results.ResultSet.Rows.length === 0) {
294
+ console.log(chalk.yellow('No results found.'));
295
+ return;
296
+ }
297
+
298
+ const rows = results.ResultSet.Rows;
299
+ const header = rows[0]?.Data?.map(d => d.VarCharValue || '') || [];
300
+ const dataRows = rows.slice(1);
301
+
302
+ // Print header
303
+ const headerLine = header.map(h => h.padEnd(20)).join(' ');
304
+ console.log(chalk.bold(chalk.cyan(headerLine)));
305
+ console.log(chalk.dim('─'.repeat(headerLine.length)));
306
+
307
+ // Print rows
308
+ dataRows.forEach(row => {
309
+ const line = (row.Data || []).map(d => (d.VarCharValue || '').substring(0, 20).padEnd(20)).join(' ');
310
+ console.log(line);
311
+ });
312
+
313
+ console.log(chalk.dim(`\n${dataRows.length} row(s)`));
314
+ } catch (error) {
315
+ printError(error.message);
316
+ process.exit(1);
317
+ }
318
+ });
319
+
320
+ // ============================================================
321
+ // DATABASES
322
+ // ============================================================
323
+
324
+ const databasesCmd = program.command('databases').description('Manage Athena databases');
325
+
326
+ databasesCmd
327
+ .command('list')
328
+ .description('List databases in the data catalog')
329
+ .option('--catalog <name>', 'Data catalog name', 'AwsDataCatalog')
330
+ .option('--json', 'Output as JSON')
331
+ .action(async (options) => {
332
+ requireAuth();
333
+ try {
334
+ const databases = await withSpinner('Fetching databases...', () =>
335
+ listDatabases(options.catalog)
336
+ );
337
+
338
+ if (options.json) {
339
+ printJson(databases);
340
+ return;
341
+ }
342
+
343
+ printTable(databases, [
344
+ { key: 'Name', label: 'Name' },
345
+ { key: 'Description', label: 'Description' },
346
+ { key: 'Parameters', label: 'Parameters', format: (v) => v ? Object.keys(v).join(', ') : '' }
347
+ ]);
348
+ } catch (error) {
349
+ printError(error.message);
350
+ process.exit(1);
351
+ }
352
+ });
353
+
354
+ databasesCmd
355
+ .command('get <database-name>')
356
+ .description('Get database details')
357
+ .option('--catalog <name>', 'Data catalog name', 'AwsDataCatalog')
358
+ .option('--json', 'Output as JSON')
359
+ .action(async (databaseName, options) => {
360
+ requireAuth();
361
+ try {
362
+ const db = await withSpinner(`Fetching database ${databaseName}...`, () =>
363
+ getDatabase(options.catalog, databaseName)
364
+ );
365
+
366
+ if (!db) {
367
+ printError('Database not found');
368
+ process.exit(1);
369
+ }
370
+
371
+ if (options.json) {
372
+ printJson(db);
373
+ return;
374
+ }
375
+
376
+ console.log(chalk.bold('\nDatabase Details\n'));
377
+ console.log('Name: ', chalk.cyan(db.Name));
378
+ console.log('Description: ', db.Description || 'N/A');
379
+ if (db.Parameters) {
380
+ console.log('Parameters: ', JSON.stringify(db.Parameters));
381
+ }
382
+ console.log('');
383
+ } catch (error) {
384
+ printError(error.message);
385
+ process.exit(1);
386
+ }
387
+ });
388
+
389
+ databasesCmd
390
+ .command('create <database-name>')
391
+ .description('Create a new database (via DDL query)')
392
+ .option('--description <desc>', 'Database description')
393
+ .option('--output <s3-path>', 'S3 output location for query results')
394
+ .option('--json', 'Output as JSON')
395
+ .action(async (databaseName, options) => {
396
+ requireAuth();
397
+ try {
398
+ const result = await withSpinner(`Creating database ${databaseName}...`, () =>
399
+ createDatabase({
400
+ databaseName,
401
+ description: options.description,
402
+ outputLocation: options.output
403
+ })
404
+ );
405
+
406
+ if (options.json) {
407
+ printJson(result);
408
+ return;
409
+ }
410
+
411
+ printSuccess(`Database creation query started`);
412
+ console.log('Execution ID: ', chalk.cyan(result?.QueryExecutionId || 'N/A'));
413
+ } catch (error) {
414
+ printError(error.message);
415
+ process.exit(1);
416
+ }
417
+ });
418
+
419
+ // ============================================================
420
+ // WORKGROUPS
421
+ // ============================================================
422
+
423
+ const workgroupsCmd = program.command('workgroups').description('Manage Athena workgroups');
424
+
425
+ workgroupsCmd
426
+ .command('list')
427
+ .description('List workgroups')
428
+ .option('--json', 'Output as JSON')
429
+ .action(async (options) => {
430
+ requireAuth();
431
+ try {
432
+ const workgroups = await withSpinner('Fetching workgroups...', () => listWorkgroups());
433
+
434
+ if (options.json) {
435
+ printJson(workgroups);
436
+ return;
437
+ }
438
+
439
+ printTable(workgroups, [
440
+ { key: 'Name', label: 'Name' },
441
+ { key: 'State', label: 'State' },
442
+ { key: 'Description', label: 'Description' },
443
+ { key: 'CreationTime', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
444
+ ]);
445
+ } catch (error) {
446
+ printError(error.message);
447
+ process.exit(1);
448
+ }
449
+ });
450
+
451
+ workgroupsCmd
452
+ .command('get <workgroup-name>')
453
+ .description('Get workgroup details')
454
+ .option('--json', 'Output as JSON')
455
+ .action(async (workgroupName, options) => {
456
+ requireAuth();
457
+ try {
458
+ const wg = await withSpinner(`Fetching workgroup ${workgroupName}...`, () =>
459
+ getWorkgroup(workgroupName)
460
+ );
461
+
462
+ if (!wg) {
463
+ printError('Workgroup not found');
464
+ process.exit(1);
465
+ }
466
+
467
+ if (options.json) {
468
+ printJson(wg);
469
+ return;
470
+ }
471
+
472
+ console.log(chalk.bold('\nWorkgroup Details\n'));
473
+ console.log('Name: ', chalk.cyan(wg.Name));
474
+ console.log('State: ', wg.State || 'N/A');
475
+ console.log('Description: ', wg.Description || 'N/A');
476
+ console.log('Created: ', wg.CreationTime ? new Date(wg.CreationTime).toLocaleString() : 'N/A');
477
+ const output = wg.Configuration?.ResultConfiguration?.OutputLocation;
478
+ if (output) console.log('Output S3: ', output);
479
+ console.log('');
480
+ } catch (error) {
481
+ printError(error.message);
482
+ process.exit(1);
483
+ }
484
+ });
485
+
486
+ workgroupsCmd
487
+ .command('create <workgroup-name>')
488
+ .description('Create a new workgroup')
489
+ .option('--description <desc>', 'Workgroup description')
490
+ .option('--output <s3-path>', 'Default S3 output location')
491
+ .option('--json', 'Output as JSON')
492
+ .action(async (workgroupName, options) => {
493
+ requireAuth();
494
+ try {
495
+ const result = await withSpinner(`Creating workgroup ${workgroupName}...`, () =>
496
+ createWorkgroup({
497
+ name: workgroupName,
498
+ description: options.description,
499
+ outputLocation: options.output
500
+ })
501
+ );
502
+
503
+ if (options.json) {
504
+ printJson(result);
505
+ return;
506
+ }
507
+
508
+ printSuccess(`Workgroup '${workgroupName}' created`);
509
+ } catch (error) {
510
+ printError(error.message);
511
+ process.exit(1);
512
+ }
513
+ });
514
+
515
+ // ============================================================
516
+ // Parse
517
+ // ============================================================
518
+
519
+ program.parse(process.argv);
520
+
521
+ if (process.argv.length <= 2) {
522
+ program.help();
523
+ }