@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 +69 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/awsamplify.js +9 -0
- package/package.json +27 -0
- package/src/api.js +202 -0
- package/src/config.js +21 -0
- package/src/index.js +500 -0
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
|
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
|
+
}
|