@ktmcp-cli/awsbatch 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,96 @@
1
+ # AGENT.md — AWS Batch CLI for AI Agents
2
+
3
+ This document explains how to use the AWS Batch CLI as an AI agent.
4
+
5
+ ## Overview
6
+
7
+ The `awsbatch` CLI provides access to the AWS Batch Computing API. Requires AWS credentials with Batch permissions.
8
+
9
+ ## Prerequisites
10
+
11
+ ```bash
12
+ awsbatch config set accessKeyId YOUR_AWS_ACCESS_KEY_ID
13
+ awsbatch config set secretAccessKey YOUR_AWS_SECRET_ACCESS_KEY
14
+ awsbatch config set region us-east-1
15
+ ```
16
+
17
+ ## All Commands
18
+
19
+ ### Config
20
+
21
+ ```bash
22
+ awsbatch config get <key>
23
+ awsbatch config set <key> <value>
24
+ awsbatch config list
25
+ ```
26
+
27
+ ### Jobs
28
+
29
+ ```bash
30
+ # Submit
31
+ awsbatch jobs submit --name my-job --queue my-queue --definition my-def
32
+ awsbatch jobs submit --name my-job --queue my-queue --definition my-def --parameters '{"key":"val"}'
33
+ awsbatch jobs submit --name my-job --queue my-queue --definition my-def --container-overrides '{"command":["cmd"]}'
34
+
35
+ # Status
36
+ awsbatch jobs get <job-id>
37
+
38
+ # List
39
+ awsbatch jobs list --queue my-queue
40
+ awsbatch jobs list --queue my-queue --status RUNNING
41
+ awsbatch jobs list --queue my-queue --status SUCCEEDED
42
+ awsbatch jobs list --queue my-queue --status FAILED
43
+
44
+ # Describe multiple
45
+ awsbatch jobs describe <job-id-1> <job-id-2>
46
+
47
+ # Terminate
48
+ awsbatch jobs terminate <job-id>
49
+ awsbatch jobs terminate <job-id> --reason "No longer needed"
50
+ ```
51
+
52
+ ### Queues
53
+
54
+ ```bash
55
+ awsbatch queues list
56
+ awsbatch queues get <queue-name>
57
+ awsbatch queues create --name <name> --state ENABLED --priority 1
58
+ awsbatch queues update <queue-name> --state DISABLED
59
+ awsbatch queues update <queue-name> --priority 10
60
+ ```
61
+
62
+ ### Job Definitions
63
+
64
+ ```bash
65
+ awsbatch definitions list
66
+ awsbatch definitions list --name <name>
67
+ awsbatch definitions list --status ACTIVE
68
+ awsbatch definitions describe <definition-name>
69
+ awsbatch definitions register --name <name> --type container --container '{"image":"img:tag","vcpus":1,"memory":512}'
70
+ ```
71
+
72
+ ## JSON Output
73
+
74
+ All commands support `--json`:
75
+
76
+ ```bash
77
+ awsbatch jobs get <id> --json
78
+ awsbatch queues list --json
79
+ awsbatch definitions list --json
80
+ ```
81
+
82
+ ## Job Status Values
83
+
84
+ - SUBMITTED — Job submitted, awaiting scheduling
85
+ - PENDING — Job pending scheduling
86
+ - RUNNABLE — Job scheduled, waiting for resources
87
+ - STARTING — Job starting
88
+ - RUNNING — Job running
89
+ - SUCCEEDED — Job completed successfully
90
+ - FAILED — Job failed
91
+
92
+ ## Error Handling
93
+
94
+ The CLI exits with code 1 on error and prints to stderr.
95
+ - `AWS authentication failed` — Check accessKeyId and secretAccessKey
96
+ - `Resource not found` — Check queue/definition names and job IDs
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,97 @@
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
+ # AWS Batch CLI
7
+
8
+ Production-ready CLI for the AWS Batch Computing API. Submit and manage batch jobs, queues, and job definitions from your terminal.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -g @ktmcp-cli/awsbatch
14
+ ```
15
+
16
+ ## Configuration
17
+
18
+ ```bash
19
+ awsbatch config set accessKeyId YOUR_AWS_ACCESS_KEY_ID
20
+ awsbatch config set secretAccessKey YOUR_AWS_SECRET_ACCESS_KEY
21
+ awsbatch config set region us-east-1
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Jobs
27
+
28
+ ```bash
29
+ # Submit a job
30
+ awsbatch jobs submit --name my-job --queue my-queue --definition my-job-def
31
+ awsbatch jobs submit --name my-job --queue my-queue --definition my-job-def \
32
+ --parameters '{"key":"value"}' \
33
+ --container-overrides '{"command":["my-script.sh"]}'
34
+
35
+ # Get job status
36
+ awsbatch jobs get <job-id>
37
+
38
+ # List jobs in a queue
39
+ awsbatch jobs list --queue my-queue
40
+ awsbatch jobs list --queue my-queue --status RUNNING
41
+ awsbatch jobs list --queue my-queue --status SUCCEEDED --limit 20
42
+
43
+ # Describe multiple jobs
44
+ awsbatch jobs describe <job-id-1> <job-id-2>
45
+
46
+ # Terminate a job
47
+ awsbatch jobs terminate <job-id>
48
+ awsbatch jobs terminate <job-id> --reason "No longer needed"
49
+ ```
50
+
51
+ ### Queues
52
+
53
+ ```bash
54
+ # List job queues
55
+ awsbatch queues list
56
+
57
+ # Get queue details
58
+ awsbatch queues get my-queue
59
+
60
+ # Create a queue
61
+ awsbatch queues create --name my-queue --priority 10
62
+ awsbatch queues create --name my-queue --state ENABLED --priority 5
63
+
64
+ # Update a queue
65
+ awsbatch queues update my-queue --state DISABLED
66
+ awsbatch queues update my-queue --priority 20
67
+ ```
68
+
69
+ ### Job Definitions
70
+
71
+ ```bash
72
+ # List job definitions
73
+ awsbatch definitions list
74
+ awsbatch definitions list --name my-definition
75
+ awsbatch definitions list --status ACTIVE
76
+
77
+ # Describe a job definition
78
+ awsbatch definitions describe my-definition
79
+
80
+ # Register a job definition
81
+ awsbatch definitions register --name my-job-def --type container \
82
+ --container '{"image":"my-image:latest","vcpus":1,"memory":512}'
83
+ ```
84
+
85
+ ### JSON Output
86
+
87
+ All commands support `--json`:
88
+
89
+ ```bash
90
+ awsbatch jobs get <id> --json
91
+ awsbatch queues list --json
92
+ awsbatch definitions list --json | jq '.[].jobDefinitionName'
93
+ ```
94
+
95
+ ## License
96
+
97
+ 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/awsbatch",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI for AWS Batch Computing API - Kill The MCP",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "awsbatch": "bin/awsbatch.js"
9
+ },
10
+ "keywords": ["awsbatch", "aws", "batch", "jobs", "compute", "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/awsbatch.git"
24
+ },
25
+ "homepage": "https://killthemcp.com/awsbatch-cli",
26
+ "bugs": { "url": "https://github.com/ktmcp-cli/awsbatch/issues" }
27
+ }
package/src/api.js ADDED
@@ -0,0 +1,230 @@
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 }) {
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 = `host:${host}\nx-amz-date:${amzDate}\n`;
32
+ const signedHeaders = 'host;x-amz-date';
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
+ function getBatchClient() {
60
+ const accessKeyId = getConfig('accessKeyId');
61
+ const secretAccessKey = getConfig('secretAccessKey');
62
+ const region = getConfig('region') || 'us-east-1';
63
+ const baseURL = `https://batch.${region}.amazonaws.com`;
64
+
65
+ return {
66
+ request: async (method, path, data = null) => {
67
+ const url = `${baseURL}${path}`;
68
+ const body = data ? JSON.stringify(data) : '';
69
+ const { authorization, amzDate } = buildAuthHeader({
70
+ method,
71
+ url,
72
+ body,
73
+ service: 'batch',
74
+ region,
75
+ accessKeyId,
76
+ secretAccessKey
77
+ });
78
+
79
+ try {
80
+ const response = await axios({
81
+ method,
82
+ url,
83
+ data: data || undefined,
84
+ headers: {
85
+ 'Authorization': authorization,
86
+ 'X-Amz-Date': amzDate,
87
+ 'Content-Type': 'application/json',
88
+ 'Accept': 'application/json'
89
+ }
90
+ });
91
+ return response.data;
92
+ } catch (error) {
93
+ handleApiError(error);
94
+ }
95
+ }
96
+ };
97
+ }
98
+
99
+ function handleApiError(error) {
100
+ if (error.response) {
101
+ const status = error.response.status;
102
+ const data = error.response.data;
103
+ if (status === 401 || status === 403) {
104
+ throw new Error('AWS authentication failed. Check your accessKeyId and secretAccessKey.');
105
+ } else if (status === 404) {
106
+ throw new Error('Resource not found.');
107
+ } else if (status === 429) {
108
+ throw new Error('Rate limit exceeded. Please wait before retrying.');
109
+ } else {
110
+ const message = data?.message || data?.Message || data?.error || JSON.stringify(data);
111
+ throw new Error(`AWS Batch Error (${status}): ${message}`);
112
+ }
113
+ } else if (error.request) {
114
+ throw new Error('No response from AWS Batch API. Check your internet connection and region.');
115
+ } else {
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ // ============================================================
121
+ // JOBS
122
+ // ============================================================
123
+
124
+ export async function submitJob({ jobName, jobQueue, jobDefinition, parameters, containerOverrides }) {
125
+ const client = getBatchClient();
126
+ const body = {
127
+ jobName,
128
+ jobQueue,
129
+ jobDefinition,
130
+ ...(parameters && { parameters }),
131
+ ...(containerOverrides && { containerOverrides })
132
+ };
133
+ const data = await client.request('POST', '/v1/submitjob', body);
134
+ return data;
135
+ }
136
+
137
+ export async function describeJobs(jobIds) {
138
+ const client = getBatchClient();
139
+ const data = await client.request('POST', '/v1/describejobs', { jobs: jobIds });
140
+ return data?.jobs || [];
141
+ }
142
+
143
+ export async function listJobs({ jobQueue, jobStatus, maxResults = 50 } = {}) {
144
+ const client = getBatchClient();
145
+ const body = {};
146
+ if (jobQueue) body.jobQueue = jobQueue;
147
+ if (jobStatus) body.jobStatus = jobStatus;
148
+ if (maxResults) body.maxResults = maxResults;
149
+ const data = await client.request('POST', '/v1/listjobs', body);
150
+ return data?.jobSummaryList || [];
151
+ }
152
+
153
+ export async function terminateJob(jobId, reason) {
154
+ const client = getBatchClient();
155
+ const data = await client.request('POST', '/v1/terminatejob', {
156
+ jobId,
157
+ reason: reason || 'Terminated via CLI'
158
+ });
159
+ return data;
160
+ }
161
+
162
+ // ============================================================
163
+ // JOB QUEUES
164
+ // ============================================================
165
+
166
+ export async function listQueues() {
167
+ const client = getBatchClient();
168
+ const data = await client.request('GET', '/v1/jobqueues');
169
+ return data?.jobQueues || [];
170
+ }
171
+
172
+ export async function getQueue(queueName) {
173
+ const client = getBatchClient();
174
+ const data = await client.request('GET', `/v1/jobqueues?jobQueues=${encodeURIComponent(queueName)}`);
175
+ return (data?.jobQueues || [])[0] || null;
176
+ }
177
+
178
+ export async function createQueue({ queueName, state, priority, computeEnvironmentOrder }) {
179
+ const client = getBatchClient();
180
+ const body = {
181
+ jobQueueName: queueName,
182
+ state: state || 'ENABLED',
183
+ priority: priority || 1,
184
+ computeEnvironmentOrder: computeEnvironmentOrder || []
185
+ };
186
+ const data = await client.request('POST', '/v1/createjobqueue', body);
187
+ return data;
188
+ }
189
+
190
+ export async function updateQueue({ queueName, state, priority }) {
191
+ const client = getBatchClient();
192
+ const body = { jobQueue: queueName };
193
+ if (state) body.state = state;
194
+ if (priority !== undefined) body.priority = priority;
195
+ const data = await client.request('POST', '/v1/updatejobqueue', body);
196
+ return data;
197
+ }
198
+
199
+ // ============================================================
200
+ // JOB DEFINITIONS
201
+ // ============================================================
202
+
203
+ export async function listDefinitions({ definitionName, status } = {}) {
204
+ const client = getBatchClient();
205
+ let path = '/v1/jobdefinitions';
206
+ const params = [];
207
+ if (definitionName) params.push(`jobDefinitionName=${encodeURIComponent(definitionName)}`);
208
+ if (status) params.push(`status=${encodeURIComponent(status)}`);
209
+ if (params.length) path += '?' + params.join('&');
210
+ const data = await client.request('GET', path);
211
+ return data?.jobDefinitions || [];
212
+ }
213
+
214
+ export async function describeDefinitions(definitionNames) {
215
+ const client = getBatchClient();
216
+ const path = `/v1/jobdefinitions?jobDefinitionName=${encodeURIComponent(definitionNames[0])}`;
217
+ const data = await client.request('GET', path);
218
+ return data?.jobDefinitions || [];
219
+ }
220
+
221
+ export async function registerDefinition({ definitionName, type, containerProperties }) {
222
+ const client = getBatchClient();
223
+ const body = {
224
+ jobDefinitionName: definitionName,
225
+ type: type || 'container',
226
+ ...(containerProperties && { containerProperties })
227
+ };
228
+ const data = await client.request('POST', '/v1/registerjobdefinition', body);
229
+ return data;
230
+ }
package/src/config.js ADDED
@@ -0,0 +1,21 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({ projectName: '@ktmcp-cli/awsbatch' });
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,576 @@
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
+ submitJob,
7
+ describeJobs,
8
+ listJobs,
9
+ terminateJob,
10
+ listQueues,
11
+ getQueue,
12
+ createQueue,
13
+ updateQueue,
14
+ listDefinitions,
15
+ describeDefinitions,
16
+ registerDefinition
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(' awsbatch config set accessKeyId YOUR_KEY'));
85
+ console.log(chalk.cyan(' awsbatch config set secretAccessKey YOUR_SECRET'));
86
+ console.log(chalk.cyan(' awsbatch config set region us-east-1'));
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ // ============================================================
92
+ // Program metadata
93
+ // ============================================================
94
+
95
+ program
96
+ .name('awsbatch')
97
+ .description(chalk.bold('AWS Batch CLI') + ' - Manage batch computing jobs 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('\nAWS Batch 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(' awsbatch config set accessKeyId YOUR_KEY'));
136
+ console.log(chalk.cyan(' awsbatch config set secretAccessKey YOUR_SECRET'));
137
+ console.log(chalk.cyan(' awsbatch 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
+ // JOBS
148
+ // ============================================================
149
+
150
+ const jobsCmd = program.command('jobs').description('Manage AWS Batch jobs');
151
+
152
+ jobsCmd
153
+ .command('submit')
154
+ .description('Submit a new batch job')
155
+ .requiredOption('--name <name>', 'Job name')
156
+ .requiredOption('--queue <queue>', 'Job queue name or ARN')
157
+ .requiredOption('--definition <def>', 'Job definition name or ARN')
158
+ .option('--parameters <json>', 'Job parameters as JSON')
159
+ .option('--container-overrides <json>', 'Container overrides as JSON')
160
+ .option('--json', 'Output as JSON')
161
+ .action(async (options) => {
162
+ requireAuth();
163
+
164
+ let parameters, containerOverrides;
165
+ if (options.parameters) {
166
+ try { parameters = JSON.parse(options.parameters); } catch { printError('Invalid JSON for --parameters'); process.exit(1); }
167
+ }
168
+ if (options.containerOverrides) {
169
+ try { containerOverrides = JSON.parse(options.containerOverrides); } catch { printError('Invalid JSON for --container-overrides'); process.exit(1); }
170
+ }
171
+
172
+ try {
173
+ const result = await withSpinner('Submitting job...', () =>
174
+ submitJob({
175
+ jobName: options.name,
176
+ jobQueue: options.queue,
177
+ jobDefinition: options.definition,
178
+ parameters,
179
+ containerOverrides
180
+ })
181
+ );
182
+
183
+ if (options.json) {
184
+ printJson(result);
185
+ return;
186
+ }
187
+
188
+ printSuccess('Job submitted');
189
+ console.log('Job ID: ', chalk.cyan(result?.jobId || 'N/A'));
190
+ console.log('Job Name: ', result?.jobName || options.name);
191
+ console.log('Job ARN: ', result?.jobArn || 'N/A');
192
+ } catch (error) {
193
+ printError(error.message);
194
+ process.exit(1);
195
+ }
196
+ });
197
+
198
+ jobsCmd
199
+ .command('get <job-id>')
200
+ .description('Get job details')
201
+ .option('--json', 'Output as JSON')
202
+ .action(async (jobId, options) => {
203
+ requireAuth();
204
+ try {
205
+ const jobs = await withSpinner(`Fetching job ${jobId}...`, () => describeJobs([jobId]));
206
+ const job = jobs[0];
207
+
208
+ if (!job) {
209
+ printError('Job not found');
210
+ process.exit(1);
211
+ }
212
+
213
+ if (options.json) {
214
+ printJson(job);
215
+ return;
216
+ }
217
+
218
+ const statusColor = job.status === 'SUCCEEDED' ? chalk.green : job.status === 'FAILED' ? chalk.red : chalk.yellow;
219
+
220
+ console.log(chalk.bold('\nJob Details\n'));
221
+ console.log('Job ID: ', chalk.cyan(job.jobId));
222
+ console.log('Job Name: ', chalk.bold(job.jobName));
223
+ console.log('Status: ', statusColor(job.status || 'N/A'));
224
+ console.log('Queue: ', job.jobQueue || 'N/A');
225
+ console.log('Definition: ', job.jobDefinition || 'N/A');
226
+ console.log('Created: ', job.createdAt ? new Date(job.createdAt).toLocaleString() : 'N/A');
227
+ console.log('Started: ', job.startedAt ? new Date(job.startedAt).toLocaleString() : 'N/A');
228
+ console.log('Stopped: ', job.stoppedAt ? new Date(job.stoppedAt).toLocaleString() : 'N/A');
229
+ if (job.statusReason) console.log('Status Reason: ', chalk.dim(job.statusReason));
230
+ if (job.container?.exitCode !== undefined) console.log('Exit Code: ', job.container.exitCode);
231
+ console.log('');
232
+ } catch (error) {
233
+ printError(error.message);
234
+ process.exit(1);
235
+ }
236
+ });
237
+
238
+ jobsCmd
239
+ .command('list')
240
+ .description('List jobs in a queue')
241
+ .requiredOption('--queue <queue>', 'Job queue name')
242
+ .option('--status <status>', 'Filter by status (SUBMITTED|PENDING|RUNNABLE|STARTING|RUNNING|SUCCEEDED|FAILED)')
243
+ .option('--limit <n>', 'Maximum number of results', '50')
244
+ .option('--json', 'Output as JSON')
245
+ .action(async (options) => {
246
+ requireAuth();
247
+ try {
248
+ const jobs = await withSpinner('Fetching jobs...', () =>
249
+ listJobs({
250
+ jobQueue: options.queue,
251
+ jobStatus: options.status,
252
+ maxResults: parseInt(options.limit)
253
+ })
254
+ );
255
+
256
+ if (options.json) {
257
+ printJson(jobs);
258
+ return;
259
+ }
260
+
261
+ printTable(jobs, [
262
+ { key: 'jobId', label: 'Job ID', format: (v) => v ? String(v).substring(0, 16) + '...' : '' },
263
+ { key: 'jobName', label: 'Name' },
264
+ { key: 'status', label: 'Status' },
265
+ { key: 'createdAt', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
266
+ ]);
267
+ } catch (error) {
268
+ printError(error.message);
269
+ process.exit(1);
270
+ }
271
+ });
272
+
273
+ jobsCmd
274
+ .command('terminate <job-id>')
275
+ .description('Terminate a job')
276
+ .option('--reason <reason>', 'Termination reason', 'Terminated via CLI')
277
+ .action(async (jobId, options) => {
278
+ requireAuth();
279
+ try {
280
+ await withSpinner(`Terminating job ${jobId}...`, () =>
281
+ terminateJob(jobId, options.reason)
282
+ );
283
+ printSuccess(`Job ${jobId} terminated`);
284
+ } catch (error) {
285
+ printError(error.message);
286
+ process.exit(1);
287
+ }
288
+ });
289
+
290
+ jobsCmd
291
+ .command('describe <job-ids...>')
292
+ .description('Describe one or more jobs by ID')
293
+ .option('--json', 'Output as JSON')
294
+ .action(async (jobIds, options) => {
295
+ requireAuth();
296
+ try {
297
+ const jobs = await withSpinner('Fetching job details...', () => describeJobs(jobIds));
298
+
299
+ if (options.json) {
300
+ printJson(jobs);
301
+ return;
302
+ }
303
+
304
+ printTable(jobs, [
305
+ { key: 'jobId', label: 'Job ID', format: (v) => v ? String(v).substring(0, 16) + '...' : '' },
306
+ { key: 'jobName', label: 'Name' },
307
+ { key: 'status', label: 'Status' },
308
+ { key: 'jobQueue', label: 'Queue', format: (v) => (v || '').split('/').pop() },
309
+ { key: 'createdAt', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
310
+ ]);
311
+ } catch (error) {
312
+ printError(error.message);
313
+ process.exit(1);
314
+ }
315
+ });
316
+
317
+ // ============================================================
318
+ // QUEUES
319
+ // ============================================================
320
+
321
+ const queuesCmd = program.command('queues').description('Manage AWS Batch job queues');
322
+
323
+ queuesCmd
324
+ .command('list')
325
+ .description('List job queues')
326
+ .option('--json', 'Output as JSON')
327
+ .action(async (options) => {
328
+ requireAuth();
329
+ try {
330
+ const queues = await withSpinner('Fetching job queues...', () => listQueues());
331
+
332
+ if (options.json) {
333
+ printJson(queues);
334
+ return;
335
+ }
336
+
337
+ printTable(queues, [
338
+ { key: 'jobQueueName', label: 'Name' },
339
+ { key: 'state', label: 'State' },
340
+ { key: 'status', label: 'Status' },
341
+ { key: 'priority', label: 'Priority', format: (v) => String(v || 0) }
342
+ ]);
343
+ } catch (error) {
344
+ printError(error.message);
345
+ process.exit(1);
346
+ }
347
+ });
348
+
349
+ queuesCmd
350
+ .command('get <queue-name>')
351
+ .description('Get job queue details')
352
+ .option('--json', 'Output as JSON')
353
+ .action(async (queueName, options) => {
354
+ requireAuth();
355
+ try {
356
+ const queue = await withSpinner(`Fetching queue ${queueName}...`, () => getQueue(queueName));
357
+
358
+ if (!queue) {
359
+ printError('Queue not found');
360
+ process.exit(1);
361
+ }
362
+
363
+ if (options.json) {
364
+ printJson(queue);
365
+ return;
366
+ }
367
+
368
+ console.log(chalk.bold('\nJob Queue Details\n'));
369
+ console.log('Name: ', chalk.cyan(queue.jobQueueName));
370
+ console.log('ARN: ', queue.jobQueueArn || 'N/A');
371
+ console.log('State: ', queue.state || 'N/A');
372
+ console.log('Status: ', queue.status || 'N/A');
373
+ console.log('Priority: ', queue.priority !== undefined ? String(queue.priority) : 'N/A');
374
+ if (queue.statusReason) console.log('Status Reason: ', queue.statusReason);
375
+ console.log('');
376
+ } catch (error) {
377
+ printError(error.message);
378
+ process.exit(1);
379
+ }
380
+ });
381
+
382
+ queuesCmd
383
+ .command('create')
384
+ .description('Create a new job queue')
385
+ .requiredOption('--name <name>', 'Queue name')
386
+ .option('--state <state>', 'Queue state (ENABLED|DISABLED)', 'ENABLED')
387
+ .option('--priority <n>', 'Queue priority (1-1000)', '1')
388
+ .option('--compute-envs <json>', 'Compute environment order as JSON array')
389
+ .option('--json', 'Output as JSON')
390
+ .action(async (options) => {
391
+ requireAuth();
392
+
393
+ let computeEnvironmentOrder = [];
394
+ if (options.computeEnvs) {
395
+ try { computeEnvironmentOrder = JSON.parse(options.computeEnvs); } catch { printError('Invalid JSON for --compute-envs'); process.exit(1); }
396
+ }
397
+
398
+ try {
399
+ const result = await withSpinner('Creating job queue...', () =>
400
+ createQueue({
401
+ queueName: options.name,
402
+ state: options.state,
403
+ priority: parseInt(options.priority),
404
+ computeEnvironmentOrder
405
+ })
406
+ );
407
+
408
+ if (options.json) {
409
+ printJson(result);
410
+ return;
411
+ }
412
+
413
+ printSuccess(`Queue '${options.name}' created`);
414
+ if (result) {
415
+ console.log('Queue ARN: ', result.jobQueueArn || 'N/A');
416
+ }
417
+ } catch (error) {
418
+ printError(error.message);
419
+ process.exit(1);
420
+ }
421
+ });
422
+
423
+ queuesCmd
424
+ .command('update <queue-name>')
425
+ .description('Update a job queue')
426
+ .option('--state <state>', 'Queue state (ENABLED|DISABLED)')
427
+ .option('--priority <n>', 'Queue priority')
428
+ .option('--json', 'Output as JSON')
429
+ .action(async (queueName, options) => {
430
+ requireAuth();
431
+ try {
432
+ const result = await withSpinner(`Updating queue ${queueName}...`, () =>
433
+ updateQueue({
434
+ queueName,
435
+ state: options.state,
436
+ priority: options.priority ? parseInt(options.priority) : undefined
437
+ })
438
+ );
439
+
440
+ if (options.json) {
441
+ printJson(result);
442
+ return;
443
+ }
444
+
445
+ printSuccess(`Queue '${queueName}' updated`);
446
+ } catch (error) {
447
+ printError(error.message);
448
+ process.exit(1);
449
+ }
450
+ });
451
+
452
+ // ============================================================
453
+ // DEFINITIONS
454
+ // ============================================================
455
+
456
+ const definitionsCmd = program.command('definitions').description('Manage AWS Batch job definitions');
457
+
458
+ definitionsCmd
459
+ .command('list')
460
+ .description('List job definitions')
461
+ .option('--name <name>', 'Filter by definition name')
462
+ .option('--status <status>', 'Filter by status (ACTIVE|INACTIVE)', 'ACTIVE')
463
+ .option('--json', 'Output as JSON')
464
+ .action(async (options) => {
465
+ requireAuth();
466
+ try {
467
+ const definitions = await withSpinner('Fetching job definitions...', () =>
468
+ listDefinitions({ definitionName: options.name, status: options.status })
469
+ );
470
+
471
+ if (options.json) {
472
+ printJson(definitions);
473
+ return;
474
+ }
475
+
476
+ printTable(definitions, [
477
+ { key: 'jobDefinitionName', label: 'Name' },
478
+ { key: 'revision', label: 'Rev', format: (v) => String(v || '') },
479
+ { key: 'type', label: 'Type' },
480
+ { key: 'status', label: 'Status' }
481
+ ]);
482
+ } catch (error) {
483
+ printError(error.message);
484
+ process.exit(1);
485
+ }
486
+ });
487
+
488
+ definitionsCmd
489
+ .command('register')
490
+ .description('Register a new job definition')
491
+ .requiredOption('--name <name>', 'Job definition name')
492
+ .option('--type <type>', 'Job type (container|multinode)', 'container')
493
+ .option('--container <json>', 'Container properties as JSON')
494
+ .option('--json', 'Output as JSON')
495
+ .action(async (options) => {
496
+ requireAuth();
497
+
498
+ let containerProperties;
499
+ if (options.container) {
500
+ try { containerProperties = JSON.parse(options.container); } catch { printError('Invalid JSON for --container'); process.exit(1); }
501
+ }
502
+
503
+ try {
504
+ const result = await withSpinner('Registering job definition...', () =>
505
+ registerDefinition({
506
+ definitionName: options.name,
507
+ type: options.type,
508
+ containerProperties
509
+ })
510
+ );
511
+
512
+ if (options.json) {
513
+ printJson(result);
514
+ return;
515
+ }
516
+
517
+ printSuccess(`Job definition '${options.name}' registered`);
518
+ if (result) {
519
+ console.log('ARN: ', result.jobDefinitionArn || 'N/A');
520
+ console.log('Revision: ', result.revision !== undefined ? String(result.revision) : 'N/A');
521
+ }
522
+ } catch (error) {
523
+ printError(error.message);
524
+ process.exit(1);
525
+ }
526
+ });
527
+
528
+ definitionsCmd
529
+ .command('describe <definition-name>')
530
+ .description('Describe a job definition')
531
+ .option('--json', 'Output as JSON')
532
+ .action(async (definitionName, options) => {
533
+ requireAuth();
534
+ try {
535
+ const definitions = await withSpinner(`Fetching definition ${definitionName}...`, () =>
536
+ describeDefinitions([definitionName])
537
+ );
538
+
539
+ if (options.json) {
540
+ printJson(definitions);
541
+ return;
542
+ }
543
+
544
+ if (!definitions || definitions.length === 0) {
545
+ printError('Job definition not found');
546
+ process.exit(1);
547
+ }
548
+
549
+ const def = definitions[0];
550
+ console.log(chalk.bold('\nJob Definition Details\n'));
551
+ console.log('Name: ', chalk.cyan(def.jobDefinitionName));
552
+ console.log('ARN: ', def.jobDefinitionArn || 'N/A');
553
+ console.log('Revision: ', def.revision !== undefined ? String(def.revision) : 'N/A');
554
+ console.log('Type: ', def.type || 'N/A');
555
+ console.log('Status: ', def.status || 'N/A');
556
+ if (def.containerProperties) {
557
+ console.log('Image: ', def.containerProperties.image || 'N/A');
558
+ console.log('vCPUs: ', def.containerProperties.vcpus || 'N/A');
559
+ console.log('Memory: ', def.containerProperties.memory ? `${def.containerProperties.memory} MB` : 'N/A');
560
+ }
561
+ console.log('');
562
+ } catch (error) {
563
+ printError(error.message);
564
+ process.exit(1);
565
+ }
566
+ });
567
+
568
+ // ============================================================
569
+ // Parse
570
+ // ============================================================
571
+
572
+ program.parse(process.argv);
573
+
574
+ if (process.argv.length <= 2) {
575
+ program.help();
576
+ }