@a9s/cli 0.0.1 → 1.0.6

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.
Files changed (95) hide show
  1. package/README.md +167 -2
  2. package/dist/scripts/seed.js +310 -0
  3. package/dist/src/App.js +476 -0
  4. package/dist/src/adapters/ServiceAdapter.js +1 -0
  5. package/dist/src/adapters/capabilities/ActionCapability.js +1 -0
  6. package/dist/src/adapters/capabilities/DetailCapability.js +1 -0
  7. package/dist/src/adapters/capabilities/EditCapability.js +1 -0
  8. package/dist/src/adapters/capabilities/YankCapability.js +42 -0
  9. package/dist/src/adapters/capabilities/YankCapability.test.js +29 -0
  10. package/dist/src/components/AdvancedTextInput.js +200 -0
  11. package/dist/src/components/AdvancedTextInput.test.js +190 -0
  12. package/dist/src/components/AutocompleteInput.js +29 -0
  13. package/dist/src/components/DetailPanel.js +12 -0
  14. package/dist/src/components/DiffViewer.js +17 -0
  15. package/dist/src/components/ErrorStatePanel.js +5 -0
  16. package/dist/src/components/HUD.js +31 -0
  17. package/dist/src/components/HelpPanel.js +33 -0
  18. package/dist/src/components/ModeBar.js +43 -0
  19. package/dist/src/components/Table/index.js +109 -0
  20. package/dist/src/components/Table/widths.js +19 -0
  21. package/dist/src/components/TableSkeleton.js +25 -0
  22. package/dist/src/components/YankHelpPanel.js +43 -0
  23. package/dist/src/constants/commands.js +15 -0
  24. package/dist/src/constants/keybindings.js +530 -0
  25. package/dist/src/constants/keys.js +37 -0
  26. package/dist/src/features/AppMainView.integration.test.js +133 -0
  27. package/dist/src/features/AppMainView.js +95 -0
  28. package/dist/src/hooks/inputEvents.js +1 -0
  29. package/dist/src/hooks/mainInputScopes.js +68 -0
  30. package/dist/src/hooks/mainInputScopes.test.js +24 -0
  31. package/dist/src/hooks/useActionController.js +78 -0
  32. package/dist/src/hooks/useAppController.js +102 -0
  33. package/dist/src/hooks/useAppController.test.js +54 -0
  34. package/dist/src/hooks/useAppData.js +48 -0
  35. package/dist/src/hooks/useAwsContext.js +77 -0
  36. package/dist/src/hooks/useAwsProfiles.js +53 -0
  37. package/dist/src/hooks/useAwsRegions.js +105 -0
  38. package/dist/src/hooks/useCommandRouter.js +56 -0
  39. package/dist/src/hooks/useCommandRouter.test.js +27 -0
  40. package/dist/src/hooks/useDetailController.js +57 -0
  41. package/dist/src/hooks/useDetailController.test.js +32 -0
  42. package/dist/src/hooks/useHelpPanel.js +65 -0
  43. package/dist/src/hooks/useHierarchyState.js +39 -0
  44. package/dist/src/hooks/useInputEventProcessor.js +450 -0
  45. package/dist/src/hooks/useInputEventProcessor.test.js +174 -0
  46. package/dist/src/hooks/useKeyChord.js +83 -0
  47. package/dist/src/hooks/useMainInput.js +18 -0
  48. package/dist/src/hooks/useNavigation.js +47 -0
  49. package/dist/src/hooks/usePendingAction.js +8 -0
  50. package/dist/src/hooks/usePickerManager.js +130 -0
  51. package/dist/src/hooks/usePickerState.js +47 -0
  52. package/dist/src/hooks/usePickerTable.js +20 -0
  53. package/dist/src/hooks/useServiceView.js +226 -0
  54. package/dist/src/hooks/useUiHints.js +60 -0
  55. package/dist/src/hooks/useYankMode.js +24 -0
  56. package/dist/src/hooks/yankHeaderMarkers.js +23 -0
  57. package/dist/src/hooks/yankHeaderMarkers.test.js +49 -0
  58. package/dist/src/index.js +30 -0
  59. package/dist/src/services.js +12 -0
  60. package/dist/src/state/atoms.js +27 -0
  61. package/dist/src/types.js +12 -0
  62. package/dist/src/utils/aws.js +39 -0
  63. package/dist/src/utils/debugLogger.js +34 -0
  64. package/dist/src/utils/secretDisplay.js +45 -0
  65. package/dist/src/utils/withFullscreen.js +38 -0
  66. package/dist/src/views/dynamodb/adapter.js +22 -0
  67. package/dist/src/views/iam/adapter.js +258 -0
  68. package/dist/src/views/iam/capabilities/detailCapability.js +93 -0
  69. package/dist/src/views/iam/capabilities/editCapability.js +59 -0
  70. package/dist/src/views/iam/capabilities/yankCapability.js +6 -0
  71. package/dist/src/views/iam/capabilities/yankOptions.js +15 -0
  72. package/dist/src/views/iam/schema.js +7 -0
  73. package/dist/src/views/iam/types.js +1 -0
  74. package/dist/src/views/iam/utils.js +21 -0
  75. package/dist/src/views/route53/adapter.js +22 -0
  76. package/dist/src/views/s3/adapter.js +154 -0
  77. package/dist/src/views/s3/capabilities/actionCapability.js +172 -0
  78. package/dist/src/views/s3/capabilities/detailCapability.js +115 -0
  79. package/dist/src/views/s3/capabilities/editCapability.js +35 -0
  80. package/dist/src/views/s3/capabilities/yankCapability.js +6 -0
  81. package/dist/src/views/s3/capabilities/yankOptions.js +55 -0
  82. package/dist/src/views/s3/client.js +12 -0
  83. package/dist/src/views/s3/fetcher.js +86 -0
  84. package/dist/src/views/s3/schema.js +6 -0
  85. package/dist/src/views/s3/utils.js +19 -0
  86. package/dist/src/views/secretsmanager/adapter.js +188 -0
  87. package/dist/src/views/secretsmanager/capabilities/actionCapability.js +193 -0
  88. package/dist/src/views/secretsmanager/capabilities/detailCapability.js +46 -0
  89. package/dist/src/views/secretsmanager/capabilities/editCapability.js +116 -0
  90. package/dist/src/views/secretsmanager/capabilities/yankCapability.js +7 -0
  91. package/dist/src/views/secretsmanager/capabilities/yankOptions.js +68 -0
  92. package/dist/src/views/secretsmanager/schema.js +28 -0
  93. package/dist/src/views/secretsmanager/types.js +1 -0
  94. package/package.json +72 -5
  95. package/index.js +0 -1
package/README.md CHANGED
@@ -1,3 +1,168 @@
1
- # @a9s/cli
1
+ # a9s
2
2
 
3
- Name reserved on npm. Official package coming soon.
3
+ ![npm version](https://img.shields.io/npm/v/@a9s/cli) ![CI](https://img.shields.io/github/actions/workflow/status/IamShobe/a9s/ci.yaml?label=CI) ![GitHub release](https://img.shields.io/github/v/release/IamShobe/a9s)
4
+
5
+ **k9s-style TUI navigator for AWS services.** Inspired by [k9s](https://github.com/derailed/k9s).
6
+
7
+ ## Installation
8
+
9
+ ### Global install (recommended)
10
+
11
+ ```shell
12
+ npm install -g @a9s/cli
13
+ a9s
14
+ ```
15
+
16
+ ### Local install + npx
17
+
18
+ ```shell
19
+ npm install @a9s/cli
20
+ npx @a9s/cli
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ Launch the TUI:
26
+
27
+ ```shell
28
+ a9s
29
+ ```
30
+
31
+ ### Navigation
32
+
33
+ - **Arrow keys / hjkl**: Navigate between rows
34
+ - **Tab**: Switch between columns (sort/filter)
35
+ - **Enter**: Drill into details or navigate to the next level
36
+ - **Backspace**: Go back to the previous level
37
+ - **/** (slash): Search/filter current view
38
+ - **:**: Command mode (e.g., `:services` to list available services)
39
+ - **?**: Show help panel with all keybindings for current context
40
+
41
+ ### Service Switching
42
+
43
+ Press `:services` to see the list of available AWS services and switch between them.
44
+
45
+ ### Common Operations
46
+
47
+ - **d**: Open detail panel for selected row (shows metadata like ARN, tags, etc.)
48
+ - **y + key**: Yank/copy shortcuts:
49
+ - `y+n` → copy name
50
+ - `y+a` → copy ARN
51
+ - `y+k` → copy S3 key or other identifiers
52
+ - **f**: Fetch/download S3 objects to local path
53
+ - **e**: Edit and upload (opens selected item in `$EDITOR`)
54
+ - **v**: Toggle reveal/hide secrets (Secrets Manager)
55
+
56
+ ## Services Supported
57
+
58
+ | Service | Status | Features |
59
+ | --------------- | ------ | ----------------------------------------------- |
60
+ | S3 | ✅ | Browse buckets, objects, download, edit, delete |
61
+ | IAM | ✅ | List users, roles, policies |
62
+ | Route 53 | ✅ | List hosted zones, records |
63
+ | Secrets Manager | ✅ | View, edit, and upload secrets |
64
+ | DynamoDB | ✅ | List tables, view items |
65
+
66
+ ## Features
67
+
68
+ - **Responsive tables** with sortable columns
69
+ - **Service switching** with `:services` command
70
+ - **VIM-inspired shortcuts** (hjkl navigation, commands)
71
+ - **Yank mode** for quick copy operations
72
+ - **Detail panels** showing rich metadata
73
+ - **In-editor editing** with upload confirmation
74
+ - **Search/filter** with `/` key
75
+ - **Help system** with context-sensitive keybindings
76
+ - **LocalStack support** for offline development
77
+
78
+ ## Development
79
+
80
+ ### Prerequisites
81
+
82
+ - Node.js 18+
83
+ - pnpm (for package management)
84
+ - Docker (for LocalStack)
85
+
86
+ ### Setup
87
+
88
+ ```bash
89
+ pnpm install
90
+ ```
91
+
92
+ ### Run Against LocalStack
93
+
94
+ Start LocalStack + seed data:
95
+
96
+ ```bash
97
+ pnpm localstack:setup
98
+ ```
99
+
100
+ Run the TUI (connects to LocalStack on port 4566):
101
+
102
+ ```bash
103
+ pnpm dev:local
104
+ ```
105
+
106
+ ### Run Against AWS
107
+
108
+ Connect to your AWS account:
109
+
110
+ ```bash
111
+ pnpm dev
112
+ ```
113
+
114
+ This will use your `~/.aws/credentials` and `AWS_REGION` environment variable.
115
+
116
+ ### Testing
117
+
118
+ ```bash
119
+ pnpm test # Run tests
120
+ pnpm typecheck # Type checking
121
+ pnpm build # Build TypeScript to dist/
122
+ ```
123
+
124
+ ### Project Structure
125
+
126
+ ```
127
+ src/
128
+ index.tsx - CLI entry point (commander)
129
+ App.tsx - Main TUI state machine & layout
130
+ types.ts - Core types (ColumnDef, TableRow, etc.)
131
+ services.ts - Service registry
132
+ adapters/ - ServiceAdapter implementations for each AWS service
133
+ views/ - Service-specific views (s3, iam, route53, dynamodb, secretsmanager)
134
+ components/ - Ink/React components (Table, HUD, DetailPanel, etc.)
135
+ hooks/ - Custom React hooks (navigation, state, etc.)
136
+ constants/ - Keybindings, commands
137
+ scripts/
138
+ seed.ts - LocalStack test data seeding
139
+ docker/
140
+ docker-compose.yml - LocalStack with services
141
+ ```
142
+
143
+ ## Goals
144
+
145
+ ### Services (Planned)
146
+
147
+ - [x] S3
148
+ - [x] IAM
149
+ - [x] Route 53
150
+ - [x] DynamoDB
151
+ - [x] Secrets Manager
152
+ - [ ] EC2
153
+ - [ ] ELB
154
+ - [ ] CloudFront
155
+
156
+ ### Features (Planned)
157
+
158
+ - [x] Responsive tables
159
+ - [x] Service switching
160
+ - [x] VIM shortcuts
161
+ - [x] Yank operations
162
+ - [x] Detail panels
163
+ - [x] Edit & upload
164
+ - [ ] Smart cross-service navigation (e.g., Route53 → ELB)
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,310 @@
1
+ import { S3Client, CreateBucketCommand, PutObjectCommand, ListBucketsCommand, } from "@aws-sdk/client-s3";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ const execFileAsync = promisify(execFile);
5
+ const client = new S3Client({
6
+ endpoint: "http://localhost:4566",
7
+ forcePathStyle: true,
8
+ region: "us-east-1",
9
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
10
+ });
11
+ const BUCKETS = [
12
+ "my-app-logs",
13
+ "data-warehouse",
14
+ "static-assets",
15
+ "backups-prod",
16
+ "ml-datasets",
17
+ "user-uploads",
18
+ "audit-trail",
19
+ "terraform-state",
20
+ "build-artifacts",
21
+ "media-storage",
22
+ ];
23
+ const OBJECT_TEMPLATES = [
24
+ "logs/{year}/{month}/app.log",
25
+ "logs/{year}/{month}/error.log",
26
+ "data/users.json",
27
+ "data/products.json",
28
+ "data/{year}/export.csv",
29
+ "config/settings.yaml",
30
+ "config/secrets.json.enc",
31
+ "reports/{year}/{month}/summary.pdf",
32
+ "images/avatars/{id}.png",
33
+ "images/banners/hero.jpg",
34
+ "scripts/deploy.sh",
35
+ "scripts/migrate.py",
36
+ "archive/{year}/backup.tar.gz",
37
+ "tmp/processing/{id}.tmp",
38
+ "public/index.html",
39
+ "public/styles.css",
40
+ "public/bundle.js",
41
+ ];
42
+ function randomInt(min, max) {
43
+ return Math.floor(Math.random() * (max - min + 1)) + min;
44
+ }
45
+ function renderTemplate(template) {
46
+ return template
47
+ .replace("{year}", String(randomInt(2022, 2024)))
48
+ .replace("{month}", String(randomInt(1, 12)).padStart(2, "0"))
49
+ .replace("{id}", Math.random().toString(36).slice(2, 10));
50
+ }
51
+ async function ensureBucket(name) {
52
+ try {
53
+ await client.send(new CreateBucketCommand({ Bucket: name }));
54
+ console.log(` Created bucket: ${name}`);
55
+ }
56
+ catch (e) {
57
+ const err = e;
58
+ if (err.name === "BucketAlreadyOwnedByYou" || err.name === "BucketAlreadyExists") {
59
+ console.log(` Bucket already exists: ${name}`);
60
+ }
61
+ else {
62
+ throw e;
63
+ }
64
+ }
65
+ }
66
+ async function seedBucket(bucket) {
67
+ const count = randomInt(0, 30);
68
+ const templates = [...OBJECT_TEMPLATES].sort(() => Math.random() - 0.5).slice(0, count);
69
+ for (const tpl of templates) {
70
+ const key = renderTemplate(tpl);
71
+ const content = `Seeded content for ${key} in ${bucket}\n`;
72
+ await client.send(new PutObjectCommand({
73
+ Bucket: bucket,
74
+ Key: key,
75
+ Body: content,
76
+ ContentType: "text/plain",
77
+ }));
78
+ process.stdout.write(".");
79
+ }
80
+ if (count > 0)
81
+ console.log(` ${count} objects`);
82
+ else
83
+ console.log(" (empty)");
84
+ }
85
+ async function checkLocalStack() {
86
+ try {
87
+ await client.send(new ListBucketsCommand({}));
88
+ }
89
+ catch {
90
+ console.error("Cannot connect to LocalStack at http://localhost:4566\n" +
91
+ "Run: pnpm localstack:up\n" +
92
+ "Wait a few seconds for it to start, then retry.");
93
+ process.exit(1);
94
+ }
95
+ }
96
+ async function runAws(args) {
97
+ const env = {
98
+ ...process.env,
99
+ AWS_ACCESS_KEY_ID: "test",
100
+ AWS_SECRET_ACCESS_KEY: "test",
101
+ AWS_DEFAULT_REGION: "us-east-1",
102
+ };
103
+ const { stdout } = await execFileAsync("aws", ["--endpoint-url", "http://localhost:4566", ...args], {
104
+ env,
105
+ timeout: 5000,
106
+ });
107
+ return stdout;
108
+ }
109
+ async function ensureManagedPolicy(policyName, policyDocument) {
110
+ const listOut = await runAws([
111
+ "iam",
112
+ "list-policies",
113
+ "--scope",
114
+ "Local",
115
+ "--query",
116
+ `Policies[?PolicyName=='${policyName}'].Arn | [0]`,
117
+ "--output",
118
+ "text",
119
+ ]);
120
+ const existingArn = listOut.trim();
121
+ if (existingArn && existingArn !== "None") {
122
+ console.log(` Managed policy already exists: ${policyName}`);
123
+ return existingArn;
124
+ }
125
+ const createOut = await runAws([
126
+ "iam",
127
+ "create-policy",
128
+ "--policy-name",
129
+ policyName,
130
+ "--policy-document",
131
+ JSON.stringify(policyDocument),
132
+ "--output",
133
+ "json",
134
+ ]);
135
+ const parsed = JSON.parse(createOut);
136
+ const arn = parsed.Policy?.Arn;
137
+ if (!arn)
138
+ throw new Error(`Failed creating managed policy ${policyName}`);
139
+ console.log(` Created managed policy: ${policyName}`);
140
+ return arn;
141
+ }
142
+ async function ensureRole(roleName, trustPolicy) {
143
+ const existing = await runAws([
144
+ "iam",
145
+ "get-role",
146
+ "--role-name",
147
+ roleName,
148
+ "--query",
149
+ "Role.RoleName",
150
+ "--output",
151
+ "text",
152
+ ]).catch(() => "");
153
+ if (existing.trim() === roleName) {
154
+ console.log(` Role already exists: ${roleName}`);
155
+ return;
156
+ }
157
+ await runAws([
158
+ "iam",
159
+ "create-role",
160
+ "--role-name",
161
+ roleName,
162
+ "--assume-role-policy-document",
163
+ JSON.stringify(trustPolicy),
164
+ ]);
165
+ console.log(` Created role: ${roleName}`);
166
+ }
167
+ async function ensureInlineRolePolicy(roleName, policyName, policyDocument) {
168
+ await runAws([
169
+ "iam",
170
+ "put-role-policy",
171
+ "--role-name",
172
+ roleName,
173
+ "--policy-name",
174
+ policyName,
175
+ "--policy-document",
176
+ JSON.stringify(policyDocument),
177
+ ]);
178
+ console.log(` Put inline policy ${policyName} on ${roleName}`);
179
+ }
180
+ async function ensureAttachedRolePolicy(roleName, policyArn) {
181
+ const attached = await runAws([
182
+ "iam",
183
+ "list-attached-role-policies",
184
+ "--role-name",
185
+ roleName,
186
+ "--query",
187
+ `AttachedPolicies[?PolicyArn=='${policyArn}'].PolicyArn | [0]`,
188
+ "--output",
189
+ "text",
190
+ ]);
191
+ if (attached.trim() === policyArn) {
192
+ console.log(` Managed policy already attached to ${roleName}`);
193
+ return;
194
+ }
195
+ await runAws(["iam", "attach-role-policy", "--role-name", roleName, "--policy-arn", policyArn]);
196
+ console.log(` Attached managed policy to ${roleName}`);
197
+ }
198
+ async function seedSecretsManager() {
199
+ console.log("\nSeeding Secrets Manager:");
200
+ const secrets = [
201
+ { name: "app/db-password", value: "s3cr3tP@ssword123" },
202
+ { name: "app/api-key", value: "sk-test-abc123xyz" },
203
+ { name: "app/jwt-secret", value: "super-secret-jwt-key-do-not-share" },
204
+ { name: "infra/config", value: JSON.stringify({ host: "localhost", port: 5432, db: "app" }) },
205
+ {
206
+ name: "prod/tls-cert",
207
+ value: "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----",
208
+ },
209
+ { name: "monitoring/grafana-api-key", value: "glsa_test123456" },
210
+ ];
211
+ for (const secret of secrets) {
212
+ try {
213
+ await runAws([
214
+ "secretsmanager",
215
+ "create-secret",
216
+ "--name",
217
+ secret.name,
218
+ "--secret-string",
219
+ secret.value,
220
+ ]);
221
+ console.log(` Created secret: ${secret.name}`);
222
+ }
223
+ catch (e) {
224
+ const err = e;
225
+ if (err.message?.includes("ResourceExistsException")) {
226
+ console.log(` Secret already exists: ${secret.name}`);
227
+ }
228
+ else {
229
+ throw e;
230
+ }
231
+ }
232
+ }
233
+ }
234
+ async function seedIam() {
235
+ console.log("\nSeeding IAM:");
236
+ const trustPolicy = {
237
+ Version: "2012-10-17",
238
+ Statement: [
239
+ {
240
+ Effect: "Allow",
241
+ Principal: { Service: "ec2.amazonaws.com" },
242
+ Action: "sts:AssumeRole",
243
+ },
244
+ ],
245
+ };
246
+ const appReadPolicy = {
247
+ Version: "2012-10-17",
248
+ Statement: [
249
+ {
250
+ Sid: "ReadSomeBuckets",
251
+ Effect: "Allow",
252
+ Action: ["s3:GetObject", "s3:ListBucket"],
253
+ Resource: [
254
+ "arn:aws:s3:::media-storage",
255
+ "arn:aws:s3:::media-storage/*",
256
+ "arn:aws:s3:::static-assets",
257
+ "arn:aws:s3:::static-assets/*",
258
+ ],
259
+ },
260
+ ],
261
+ };
262
+ const auditPolicy = {
263
+ Version: "2012-10-17",
264
+ Statement: [
265
+ {
266
+ Sid: "AuditReadOnly",
267
+ Effect: "Allow",
268
+ Action: ["s3:GetObject", "s3:ListBucket"],
269
+ Resource: ["arn:aws:s3:::audit-trail", "arn:aws:s3:::audit-trail/*"],
270
+ },
271
+ ],
272
+ };
273
+ const inlineDeployPolicy = {
274
+ Version: "2012-10-17",
275
+ Statement: [
276
+ {
277
+ Sid: "WriteBuildArtifacts",
278
+ Effect: "Allow",
279
+ Action: ["s3:PutObject", "s3:DeleteObject", "s3:GetObject"],
280
+ Resource: ["arn:aws:s3:::build-artifacts/*"],
281
+ },
282
+ ],
283
+ };
284
+ const readPolicyArn = await ensureManagedPolicy("A9SAppReadPolicy", appReadPolicy);
285
+ const auditPolicyArn = await ensureManagedPolicy("A9SAuditPolicy", auditPolicy);
286
+ await ensureRole("A9SAppRole", trustPolicy);
287
+ await ensureRole("A9SReadOnlyRole", trustPolicy);
288
+ await ensureInlineRolePolicy("A9SAppRole", "A9SInlineDeployPolicy", inlineDeployPolicy);
289
+ await ensureAttachedRolePolicy("A9SAppRole", readPolicyArn);
290
+ await ensureAttachedRolePolicy("A9SReadOnlyRole", auditPolicyArn);
291
+ }
292
+ async function main() {
293
+ console.log("Checking LocalStack connection...");
294
+ await checkLocalStack();
295
+ console.log("Connected!\n");
296
+ for (const bucket of BUCKETS) {
297
+ console.log(`Seeding ${bucket}:`);
298
+ await ensureBucket(bucket);
299
+ process.stdout.write(" Objects: ");
300
+ await seedBucket(bucket);
301
+ }
302
+ await seedIam();
303
+ await seedSecretsManager();
304
+ console.log("\nDone! LocalStack seeded with test data.");
305
+ console.log("Run: pnpm dev:local");
306
+ }
307
+ main().catch((e) => {
308
+ console.error(e);
309
+ process.exit(1);
310
+ });