@ktmcp-cli/awsamplify 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,69 @@
1
+ # AGENT.md — AWS Amplify CLI for AI Agents
2
+
3
+ This document explains how to use the AWS Amplify CLI as an AI agent.
4
+
5
+ ## Overview
6
+
7
+ The `awsamplify` CLI provides access to the AWS Amplify API. Requires AWS credentials with Amplify permissions.
8
+
9
+ ## Prerequisites
10
+
11
+ ```bash
12
+ awsamplify config set accessKeyId YOUR_AWS_ACCESS_KEY_ID
13
+ awsamplify config set secretAccessKey YOUR_AWS_SECRET_ACCESS_KEY
14
+ awsamplify config set region us-east-1
15
+ ```
16
+
17
+ ## All Commands
18
+
19
+ ### Config
20
+
21
+ ```bash
22
+ awsamplify config get <key>
23
+ awsamplify config set <key> <value>
24
+ awsamplify config list
25
+ ```
26
+
27
+ ### Apps
28
+
29
+ ```bash
30
+ awsamplify apps list
31
+ awsamplify apps list --region us-west-2
32
+ awsamplify apps get <app-id>
33
+ awsamplify apps create --name "my-app"
34
+ awsamplify apps create --name "my-app" --description "desc" --repository https://github.com/user/repo
35
+ awsamplify apps delete <app-id>
36
+ ```
37
+
38
+ ### Branches
39
+
40
+ ```bash
41
+ awsamplify branches list <app-id>
42
+ awsamplify branches get <app-id> <branch-name>
43
+ awsamplify branches create <app-id> --name main
44
+ awsamplify branches delete <app-id> <branch-name>
45
+ ```
46
+
47
+ ### Deployments
48
+
49
+ ```bash
50
+ awsamplify deployments list <app-id> <branch-name>
51
+ awsamplify deployments get <app-id> <branch-name> <job-id>
52
+ awsamplify deployments create <app-id> <branch-name>
53
+ ```
54
+
55
+ ## JSON Output
56
+
57
+ All commands support `--json`:
58
+
59
+ ```bash
60
+ awsamplify apps list --json
61
+ awsamplify branches list <app-id> --json
62
+ awsamplify deployments list <app-id> main --json
63
+ ```
64
+
65
+ ## Error Handling
66
+
67
+ The CLI exits with code 1 on error and prints to stderr.
68
+ - `AWS authentication failed` — Check accessKeyId and secretAccessKey
69
+ - `Resource not found` — Check the ID is correct
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
+ # AWS Amplify CLI
7
+
8
+ Production-ready CLI for the AWS Amplify API. Manage apps, branches, and deployments directly from your terminal.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -g @ktmcp-cli/awsamplify
14
+ ```
15
+
16
+ ## Configuration
17
+
18
+ ```bash
19
+ awsamplify config set accessKeyId YOUR_AWS_ACCESS_KEY_ID
20
+ awsamplify config set secretAccessKey YOUR_AWS_SECRET_ACCESS_KEY
21
+ awsamplify config set region us-east-1
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Apps
27
+
28
+ ```bash
29
+ # List all Amplify apps
30
+ awsamplify apps list
31
+ awsamplify apps list --region us-west-2
32
+
33
+ # Get app details
34
+ awsamplify apps get <app-id>
35
+
36
+ # Create a new app
37
+ awsamplify apps create --name "my-app"
38
+ awsamplify apps create --name "my-app" --description "My Amplify app" --repository https://github.com/user/repo
39
+
40
+ # Delete an app
41
+ awsamplify apps delete <app-id>
42
+ ```
43
+
44
+ ### Branches
45
+
46
+ ```bash
47
+ # List branches for an app
48
+ awsamplify branches list <app-id>
49
+
50
+ # Get branch details
51
+ awsamplify branches get <app-id> main
52
+
53
+ # Create a branch
54
+ awsamplify branches create <app-id> --name main
55
+ awsamplify branches create <app-id> --name staging --description "Staging branch"
56
+
57
+ # Delete a branch
58
+ awsamplify branches delete <app-id> <branch-name>
59
+ ```
60
+
61
+ ### Deployments
62
+
63
+ ```bash
64
+ # List deployments (jobs) for a branch
65
+ awsamplify deployments list <app-id> main
66
+
67
+ # Get deployment details
68
+ awsamplify deployments get <app-id> main <job-id>
69
+
70
+ # Trigger a new deployment
71
+ awsamplify deployments create <app-id> main
72
+ ```
73
+
74
+ ### JSON Output
75
+
76
+ All commands support `--json`:
77
+
78
+ ```bash
79
+ awsamplify apps list --json
80
+ awsamplify branches list <app-id> --json
81
+ awsamplify deployments list <app-id> main --json | jq '.[].status'
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/awsamplify",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI for AWS Amplify API - Kill The MCP",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "awsamplify": "bin/awsamplify.js"
9
+ },
10
+ "keywords": ["awsamplify", "aws", "amplify", "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/awsamplify.git"
24
+ },
25
+ "homepage": "https://killthemcp.com/awsamplify-cli",
26
+ "bugs": { "url": "https://github.com/ktmcp-cli/awsamplify/issues" }
27
+ }
package/src/api.js ADDED
@@ -0,0 +1,202 @@
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
+ const kSigning = sign(kService, 'aws4_request');
17
+ return kSigning;
18
+ }
19
+
20
+ function buildAuthHeader({ method, url, body, service, region, accessKeyId, secretAccessKey }) {
21
+ const parsedUrl = new URL(url);
22
+ const host = parsedUrl.host;
23
+ const path = parsedUrl.pathname;
24
+ const queryString = parsedUrl.search ? parsedUrl.search.slice(1) : '';
25
+
26
+ const now = new Date();
27
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '').slice(0, 15) + 'Z';
28
+ const dateStamp = amzDate.slice(0, 8);
29
+
30
+ const payloadHash = crypto.createHash('sha256').update(body || '').digest('hex');
31
+
32
+ const canonicalHeaders = `host:${host}\nx-amz-date:${amzDate}\n`;
33
+ const signedHeaders = 'host;x-amz-date';
34
+
35
+ const canonicalRequest = [
36
+ method.toUpperCase(),
37
+ path,
38
+ queryString,
39
+ canonicalHeaders,
40
+ signedHeaders,
41
+ payloadHash
42
+ ].join('\n');
43
+
44
+ const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
45
+ const stringToSign = [
46
+ 'AWS4-HMAC-SHA256',
47
+ amzDate,
48
+ credentialScope,
49
+ crypto.createHash('sha256').update(canonicalRequest).digest('hex')
50
+ ].join('\n');
51
+
52
+ const signingKey = getSigningKey(secretAccessKey, dateStamp, region, service);
53
+ const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');
54
+
55
+ const authorization = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
56
+
57
+ return { authorization, amzDate };
58
+ }
59
+
60
+ function getAwsClient(region) {
61
+ const accessKeyId = getConfig('accessKeyId');
62
+ const secretAccessKey = getConfig('secretAccessKey');
63
+ const resolvedRegion = region || getConfig('region') || 'us-east-1';
64
+ const baseURL = `https://amplify.${resolvedRegion}.amazonaws.com`;
65
+
66
+ return {
67
+ request: async (method, path, data = null) => {
68
+ const url = `${baseURL}${path}`;
69
+ const body = data ? JSON.stringify(data) : '';
70
+ const { authorization, amzDate } = buildAuthHeader({
71
+ method,
72
+ url,
73
+ body,
74
+ service: 'amplify',
75
+ region: resolvedRegion,
76
+ accessKeyId,
77
+ secretAccessKey
78
+ });
79
+
80
+ try {
81
+ const response = await axios({
82
+ method,
83
+ url,
84
+ data: data || undefined,
85
+ headers: {
86
+ 'Authorization': authorization,
87
+ 'X-Amz-Date': amzDate,
88
+ 'Content-Type': 'application/json',
89
+ 'Accept': 'application/json'
90
+ }
91
+ });
92
+ return response.data;
93
+ } catch (error) {
94
+ handleApiError(error);
95
+ }
96
+ }
97
+ };
98
+ }
99
+
100
+ function handleApiError(error) {
101
+ if (error.response) {
102
+ const status = error.response.status;
103
+ const data = error.response.data;
104
+ if (status === 401 || status === 403) {
105
+ throw new Error('AWS authentication failed. Check your accessKeyId and secretAccessKey.');
106
+ } else if (status === 404) {
107
+ throw new Error('Resource not found.');
108
+ } else if (status === 429) {
109
+ throw new Error('Rate limit exceeded. Please wait before retrying.');
110
+ } else {
111
+ const message = data?.message || data?.Message || data?.error || JSON.stringify(data);
112
+ throw new Error(`AWS API Error (${status}): ${message}`);
113
+ }
114
+ } else if (error.request) {
115
+ throw new Error('No response from AWS Amplify API. Check your internet connection and region.');
116
+ } else {
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ // ============================================================
122
+ // APPS
123
+ // ============================================================
124
+
125
+ export async function listApps(region) {
126
+ const client = getAwsClient(region);
127
+ const data = await client.request('GET', '/apps');
128
+ return data?.apps || [];
129
+ }
130
+
131
+ export async function getApp(appId, region) {
132
+ const client = getAwsClient(region);
133
+ const data = await client.request('GET', `/apps/${appId}`);
134
+ return data?.app || data || null;
135
+ }
136
+
137
+ export async function createApp({ name, description, repository, region }) {
138
+ const client = getAwsClient(region);
139
+ const body = { name };
140
+ if (description) body.description = description;
141
+ if (repository) body.repository = repository;
142
+ const data = await client.request('POST', '/apps', body);
143
+ return data?.app || data || null;
144
+ }
145
+
146
+ export async function deleteApp(appId, region) {
147
+ const client = getAwsClient(region);
148
+ const data = await client.request('DELETE', `/apps/${appId}`);
149
+ return data?.app || data || null;
150
+ }
151
+
152
+ // ============================================================
153
+ // BRANCHES
154
+ // ============================================================
155
+
156
+ export async function listBranches(appId, region) {
157
+ const client = getAwsClient(region);
158
+ const data = await client.request('GET', `/apps/${appId}/branches`);
159
+ return data?.branches || [];
160
+ }
161
+
162
+ export async function getBranch(appId, branchName, region) {
163
+ const client = getAwsClient(region);
164
+ const data = await client.request('GET', `/apps/${appId}/branches/${encodeURIComponent(branchName)}`);
165
+ return data?.branch || data || null;
166
+ }
167
+
168
+ export async function createBranch({ appId, branchName, description, region }) {
169
+ const client = getAwsClient(region);
170
+ const body = { branchName };
171
+ if (description) body.description = description;
172
+ const data = await client.request('POST', `/apps/${appId}/branches`, body);
173
+ return data?.branch || data || null;
174
+ }
175
+
176
+ export async function deleteBranch(appId, branchName, region) {
177
+ const client = getAwsClient(region);
178
+ const data = await client.request('DELETE', `/apps/${appId}/branches/${encodeURIComponent(branchName)}`);
179
+ return data?.branch || data || null;
180
+ }
181
+
182
+ // ============================================================
183
+ // DEPLOYMENTS
184
+ // ============================================================
185
+
186
+ export async function listDeployments(appId, branchName, region) {
187
+ const client = getAwsClient(region);
188
+ const data = await client.request('GET', `/apps/${appId}/branches/${encodeURIComponent(branchName)}/jobs`);
189
+ return data?.jobSummaries || [];
190
+ }
191
+
192
+ export async function getDeployment(appId, branchName, jobId, region) {
193
+ const client = getAwsClient(region);
194
+ const data = await client.request('GET', `/apps/${appId}/branches/${encodeURIComponent(branchName)}/jobs/${jobId}`);
195
+ return data?.job || data || null;
196
+ }
197
+
198
+ export async function createDeployment({ appId, branchName, region }) {
199
+ const client = getAwsClient(region);
200
+ const data = await client.request('POST', `/apps/${appId}/branches/${encodeURIComponent(branchName)}/deployments`);
201
+ return data || null;
202
+ }
package/src/config.js ADDED
@@ -0,0 +1,21 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({ projectName: '@ktmcp-cli/awsamplify' });
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,500 @@
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
+ listApps,
7
+ getApp,
8
+ createApp,
9
+ deleteApp,
10
+ listBranches,
11
+ getBranch,
12
+ createBranch,
13
+ deleteBranch,
14
+ listDeployments,
15
+ getDeployment,
16
+ createDeployment
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(' awsamplify config set accessKeyId YOUR_KEY'));
85
+ console.log(chalk.cyan(' awsamplify config set secretAccessKey YOUR_SECRET'));
86
+ console.log(chalk.cyan(' awsamplify config set region us-east-1'));
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ // ============================================================
92
+ // Program metadata
93
+ // ============================================================
94
+
95
+ program
96
+ .name('awsamplify')
97
+ .description(chalk.bold('AWS Amplify CLI') + ' - Manage Amplify apps, branches, and deployments')
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 Amplify 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(' awsamplify config set accessKeyId YOUR_KEY'));
136
+ console.log(chalk.cyan(' awsamplify config set secretAccessKey YOUR_SECRET'));
137
+ console.log(chalk.cyan(' awsamplify 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
+ // APPS
148
+ // ============================================================
149
+
150
+ const appsCmd = program.command('apps').description('Manage Amplify apps');
151
+
152
+ appsCmd
153
+ .command('list')
154
+ .description('List all Amplify apps')
155
+ .option('--region <region>', 'AWS region')
156
+ .option('--json', 'Output as JSON')
157
+ .action(async (options) => {
158
+ requireAuth();
159
+ try {
160
+ const apps = await withSpinner('Fetching Amplify apps...', () =>
161
+ listApps(options.region)
162
+ );
163
+
164
+ if (options.json) {
165
+ printJson(apps);
166
+ return;
167
+ }
168
+
169
+ printTable(apps, [
170
+ { key: 'appId', label: 'App ID' },
171
+ { key: 'name', label: 'Name' },
172
+ { key: 'description', label: 'Description' },
173
+ { key: 'defaultDomain', label: 'Domain' },
174
+ { key: 'createTime', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
175
+ ]);
176
+ } catch (error) {
177
+ printError(error.message);
178
+ process.exit(1);
179
+ }
180
+ });
181
+
182
+ appsCmd
183
+ .command('get <app-id>')
184
+ .description('Get details of an Amplify app')
185
+ .option('--region <region>', 'AWS region')
186
+ .option('--json', 'Output as JSON')
187
+ .action(async (appId, options) => {
188
+ requireAuth();
189
+ try {
190
+ const app = await withSpinner(`Fetching app ${appId}...`, () =>
191
+ getApp(appId, options.region)
192
+ );
193
+
194
+ if (!app) {
195
+ printError('App not found');
196
+ process.exit(1);
197
+ }
198
+
199
+ if (options.json) {
200
+ printJson(app);
201
+ return;
202
+ }
203
+
204
+ console.log(chalk.bold('\nAmplify App Details\n'));
205
+ console.log('App ID: ', chalk.cyan(app.appId || appId));
206
+ console.log('Name: ', chalk.bold(app.name || 'N/A'));
207
+ console.log('Description: ', app.description || 'N/A');
208
+ console.log('Domain: ', app.defaultDomain || 'N/A');
209
+ console.log('Repository: ', app.repository || 'N/A');
210
+ console.log('Platform: ', app.platform || 'N/A');
211
+ console.log('Created: ', app.createTime ? new Date(app.createTime).toLocaleString() : 'N/A');
212
+ console.log('Updated: ', app.updateTime ? new Date(app.updateTime).toLocaleString() : 'N/A');
213
+ console.log('');
214
+ } catch (error) {
215
+ printError(error.message);
216
+ process.exit(1);
217
+ }
218
+ });
219
+
220
+ appsCmd
221
+ .command('create')
222
+ .description('Create a new Amplify app')
223
+ .requiredOption('--name <name>', 'App name')
224
+ .option('--description <description>', 'App description')
225
+ .option('--repository <url>', 'Repository URL')
226
+ .option('--region <region>', 'AWS region')
227
+ .option('--json', 'Output as JSON')
228
+ .action(async (options) => {
229
+ requireAuth();
230
+ try {
231
+ const app = await withSpinner('Creating Amplify app...', () =>
232
+ createApp({
233
+ name: options.name,
234
+ description: options.description,
235
+ repository: options.repository,
236
+ region: options.region
237
+ })
238
+ );
239
+
240
+ if (options.json) {
241
+ printJson(app);
242
+ return;
243
+ }
244
+
245
+ printSuccess(`App created: ${chalk.bold(app?.name || options.name)}`);
246
+ if (app) {
247
+ console.log('App ID: ', chalk.cyan(app.appId));
248
+ console.log('Domain: ', app.defaultDomain || 'N/A');
249
+ }
250
+ } catch (error) {
251
+ printError(error.message);
252
+ process.exit(1);
253
+ }
254
+ });
255
+
256
+ appsCmd
257
+ .command('delete <app-id>')
258
+ .description('Delete an Amplify app')
259
+ .option('--region <region>', 'AWS region')
260
+ .action(async (appId, options) => {
261
+ requireAuth();
262
+ try {
263
+ await withSpinner(`Deleting app ${appId}...`, () => deleteApp(appId, options.region));
264
+ printSuccess(`App ${appId} deleted`);
265
+ } catch (error) {
266
+ printError(error.message);
267
+ process.exit(1);
268
+ }
269
+ });
270
+
271
+ // ============================================================
272
+ // BRANCHES
273
+ // ============================================================
274
+
275
+ const branchesCmd = program.command('branches').description('Manage Amplify branches');
276
+
277
+ branchesCmd
278
+ .command('list <app-id>')
279
+ .description('List branches for an app')
280
+ .option('--region <region>', 'AWS region')
281
+ .option('--json', 'Output as JSON')
282
+ .action(async (appId, options) => {
283
+ requireAuth();
284
+ try {
285
+ const branches = await withSpinner('Fetching branches...', () =>
286
+ listBranches(appId, options.region)
287
+ );
288
+
289
+ if (options.json) {
290
+ printJson(branches);
291
+ return;
292
+ }
293
+
294
+ printTable(branches, [
295
+ { key: 'branchName', label: 'Branch' },
296
+ { key: 'displayName', label: 'Display Name' },
297
+ { key: 'stage', label: 'Stage' },
298
+ { key: 'activeJobId', label: 'Active Job' },
299
+ { key: 'createTime', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
300
+ ]);
301
+ } catch (error) {
302
+ printError(error.message);
303
+ process.exit(1);
304
+ }
305
+ });
306
+
307
+ branchesCmd
308
+ .command('get <app-id> <branch-name>')
309
+ .description('Get branch details')
310
+ .option('--region <region>', 'AWS region')
311
+ .option('--json', 'Output as JSON')
312
+ .action(async (appId, branchName, options) => {
313
+ requireAuth();
314
+ try {
315
+ const branch = await withSpinner(`Fetching branch ${branchName}...`, () =>
316
+ getBranch(appId, branchName, options.region)
317
+ );
318
+
319
+ if (!branch) {
320
+ printError('Branch not found');
321
+ process.exit(1);
322
+ }
323
+
324
+ if (options.json) {
325
+ printJson(branch);
326
+ return;
327
+ }
328
+
329
+ console.log(chalk.bold('\nBranch Details\n'));
330
+ console.log('Branch Name: ', chalk.cyan(branch.branchName || branchName));
331
+ console.log('Display Name: ', branch.displayName || 'N/A');
332
+ console.log('Stage: ', branch.stage || 'N/A');
333
+ console.log('Active Job: ', branch.activeJobId || 'N/A');
334
+ console.log('Framework: ', branch.framework || 'N/A');
335
+ console.log('Created: ', branch.createTime ? new Date(branch.createTime).toLocaleString() : 'N/A');
336
+ console.log('');
337
+ } catch (error) {
338
+ printError(error.message);
339
+ process.exit(1);
340
+ }
341
+ });
342
+
343
+ branchesCmd
344
+ .command('create <app-id>')
345
+ .description('Create a new branch')
346
+ .requiredOption('--name <name>', 'Branch name')
347
+ .option('--description <description>', 'Branch description')
348
+ .option('--region <region>', 'AWS region')
349
+ .option('--json', 'Output as JSON')
350
+ .action(async (appId, options) => {
351
+ requireAuth();
352
+ try {
353
+ const branch = await withSpinner('Creating branch...', () =>
354
+ createBranch({
355
+ appId,
356
+ branchName: options.name,
357
+ description: options.description,
358
+ region: options.region
359
+ })
360
+ );
361
+
362
+ if (options.json) {
363
+ printJson(branch);
364
+ return;
365
+ }
366
+
367
+ printSuccess(`Branch created: ${chalk.bold(options.name)}`);
368
+ if (branch) console.log('Branch ARN: ', branch.branchArn || 'N/A');
369
+ } catch (error) {
370
+ printError(error.message);
371
+ process.exit(1);
372
+ }
373
+ });
374
+
375
+ branchesCmd
376
+ .command('delete <app-id> <branch-name>')
377
+ .description('Delete a branch')
378
+ .option('--region <region>', 'AWS region')
379
+ .action(async (appId, branchName, options) => {
380
+ requireAuth();
381
+ try {
382
+ await withSpinner(`Deleting branch ${branchName}...`, () =>
383
+ deleteBranch(appId, branchName, options.region)
384
+ );
385
+ printSuccess(`Branch ${branchName} deleted`);
386
+ } catch (error) {
387
+ printError(error.message);
388
+ process.exit(1);
389
+ }
390
+ });
391
+
392
+ // ============================================================
393
+ // DEPLOYMENTS
394
+ // ============================================================
395
+
396
+ const deploymentsCmd = program.command('deployments').description('Manage Amplify deployments');
397
+
398
+ deploymentsCmd
399
+ .command('list <app-id> <branch-name>')
400
+ .description('List deployments (jobs) for a branch')
401
+ .option('--region <region>', 'AWS region')
402
+ .option('--json', 'Output as JSON')
403
+ .action(async (appId, branchName, options) => {
404
+ requireAuth();
405
+ try {
406
+ const deployments = await withSpinner('Fetching deployments...', () =>
407
+ listDeployments(appId, branchName, options.region)
408
+ );
409
+
410
+ if (options.json) {
411
+ printJson(deployments);
412
+ return;
413
+ }
414
+
415
+ printTable(deployments, [
416
+ { key: 'jobId', label: 'Job ID' },
417
+ { key: 'jobType', label: 'Type' },
418
+ { key: 'status', label: 'Status' },
419
+ { key: 'commitMessage', label: 'Commit', format: (v) => (v || '').substring(0, 30) },
420
+ { key: 'startTime', label: 'Started', format: (v) => v ? new Date(v).toLocaleDateString() : '' },
421
+ { key: 'endTime', label: 'Ended', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
422
+ ]);
423
+ } catch (error) {
424
+ printError(error.message);
425
+ process.exit(1);
426
+ }
427
+ });
428
+
429
+ deploymentsCmd
430
+ .command('get <app-id> <branch-name> <job-id>')
431
+ .description('Get deployment details')
432
+ .option('--region <region>', 'AWS region')
433
+ .option('--json', 'Output as JSON')
434
+ .action(async (appId, branchName, jobId, options) => {
435
+ requireAuth();
436
+ try {
437
+ const deployment = await withSpinner(`Fetching deployment ${jobId}...`, () =>
438
+ getDeployment(appId, branchName, jobId, options.region)
439
+ );
440
+
441
+ if (!deployment) {
442
+ printError('Deployment not found');
443
+ process.exit(1);
444
+ }
445
+
446
+ if (options.json) {
447
+ printJson(deployment);
448
+ return;
449
+ }
450
+
451
+ const summary = deployment.summary || deployment;
452
+ console.log(chalk.bold('\nDeployment Details\n'));
453
+ console.log('Job ID: ', chalk.cyan(summary.jobId || jobId));
454
+ console.log('Type: ', summary.jobType || 'N/A');
455
+ console.log('Status: ', summary.status || 'N/A');
456
+ console.log('Commit ID: ', summary.commitId || 'N/A');
457
+ console.log('Commit Msg: ', summary.commitMessage || 'N/A');
458
+ console.log('Started: ', summary.startTime ? new Date(summary.startTime).toLocaleString() : 'N/A');
459
+ console.log('Ended: ', summary.endTime ? new Date(summary.endTime).toLocaleString() : 'N/A');
460
+ console.log('');
461
+ } catch (error) {
462
+ printError(error.message);
463
+ process.exit(1);
464
+ }
465
+ });
466
+
467
+ deploymentsCmd
468
+ .command('create <app-id> <branch-name>')
469
+ .description('Trigger a new deployment for a branch')
470
+ .option('--region <region>', 'AWS region')
471
+ .option('--json', 'Output as JSON')
472
+ .action(async (appId, branchName, options) => {
473
+ requireAuth();
474
+ try {
475
+ const result = await withSpinner('Creating deployment...', () =>
476
+ createDeployment({ appId, branchName, region: options.region })
477
+ );
478
+
479
+ if (options.json) {
480
+ printJson(result);
481
+ return;
482
+ }
483
+
484
+ printSuccess('Deployment created');
485
+ if (result) console.log(JSON.stringify(result, null, 2));
486
+ } catch (error) {
487
+ printError(error.message);
488
+ process.exit(1);
489
+ }
490
+ });
491
+
492
+ // ============================================================
493
+ // Parse
494
+ // ============================================================
495
+
496
+ program.parse(process.argv);
497
+
498
+ if (process.argv.length <= 2) {
499
+ program.help();
500
+ }