@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 +93 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/awsathena.js +9 -0
- package/package.json +27 -0
- package/src/api.js +206 -0
- package/src/config.js +21 -0
- package/src/index.js +523 -0
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
|
package/bin/awsathena.js
ADDED
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
|
+
}
|