@a9s/cli 0.0.1 → 0.1.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/README.md +167 -2
- package/dist/scripts/seed.js +310 -0
- package/dist/src/App.js +476 -0
- package/dist/src/adapters/ServiceAdapter.js +1 -0
- package/dist/src/adapters/capabilities/ActionCapability.js +1 -0
- package/dist/src/adapters/capabilities/DetailCapability.js +1 -0
- package/dist/src/adapters/capabilities/EditCapability.js +1 -0
- package/dist/src/adapters/capabilities/YankCapability.js +42 -0
- package/dist/src/adapters/capabilities/YankCapability.test.js +29 -0
- package/dist/src/components/AdvancedTextInput.js +200 -0
- package/dist/src/components/AdvancedTextInput.test.js +190 -0
- package/dist/src/components/AutocompleteInput.js +29 -0
- package/dist/src/components/DetailPanel.js +12 -0
- package/dist/src/components/DiffViewer.js +17 -0
- package/dist/src/components/ErrorStatePanel.js +5 -0
- package/dist/src/components/HUD.js +31 -0
- package/dist/src/components/HelpPanel.js +33 -0
- package/dist/src/components/ModeBar.js +43 -0
- package/dist/src/components/Table/index.js +109 -0
- package/dist/src/components/Table/widths.js +19 -0
- package/dist/src/components/TableSkeleton.js +25 -0
- package/dist/src/components/YankHelpPanel.js +43 -0
- package/dist/src/constants/commands.js +15 -0
- package/dist/src/constants/keybindings.js +530 -0
- package/dist/src/constants/keys.js +37 -0
- package/dist/src/features/AppMainView.integration.test.js +133 -0
- package/dist/src/features/AppMainView.js +95 -0
- package/dist/src/hooks/inputEvents.js +1 -0
- package/dist/src/hooks/mainInputScopes.js +68 -0
- package/dist/src/hooks/mainInputScopes.test.js +24 -0
- package/dist/src/hooks/useActionController.js +78 -0
- package/dist/src/hooks/useAppController.js +102 -0
- package/dist/src/hooks/useAppController.test.js +54 -0
- package/dist/src/hooks/useAppData.js +48 -0
- package/dist/src/hooks/useAwsContext.js +77 -0
- package/dist/src/hooks/useAwsProfiles.js +53 -0
- package/dist/src/hooks/useAwsRegions.js +105 -0
- package/dist/src/hooks/useCommandRouter.js +56 -0
- package/dist/src/hooks/useCommandRouter.test.js +27 -0
- package/dist/src/hooks/useDetailController.js +57 -0
- package/dist/src/hooks/useDetailController.test.js +32 -0
- package/dist/src/hooks/useHelpPanel.js +65 -0
- package/dist/src/hooks/useHierarchyState.js +39 -0
- package/dist/src/hooks/useInputEventProcessor.js +450 -0
- package/dist/src/hooks/useInputEventProcessor.test.js +174 -0
- package/dist/src/hooks/useKeyChord.js +83 -0
- package/dist/src/hooks/useMainInput.js +18 -0
- package/dist/src/hooks/useNavigation.js +47 -0
- package/dist/src/hooks/usePendingAction.js +8 -0
- package/dist/src/hooks/usePickerManager.js +130 -0
- package/dist/src/hooks/usePickerState.js +47 -0
- package/dist/src/hooks/usePickerTable.js +20 -0
- package/dist/src/hooks/useServiceView.js +226 -0
- package/dist/src/hooks/useUiHints.js +60 -0
- package/dist/src/hooks/useYankMode.js +24 -0
- package/dist/src/hooks/yankHeaderMarkers.js +23 -0
- package/dist/src/hooks/yankHeaderMarkers.test.js +49 -0
- package/dist/src/index.js +30 -0
- package/dist/src/services.js +12 -0
- package/dist/src/state/atoms.js +27 -0
- package/dist/src/types.js +12 -0
- package/dist/src/utils/aws.js +39 -0
- package/dist/src/utils/debugLogger.js +34 -0
- package/dist/src/utils/secretDisplay.js +45 -0
- package/dist/src/utils/withFullscreen.js +38 -0
- package/dist/src/views/dynamodb/adapter.js +22 -0
- package/dist/src/views/iam/adapter.js +258 -0
- package/dist/src/views/iam/capabilities/detailCapability.js +93 -0
- package/dist/src/views/iam/capabilities/editCapability.js +59 -0
- package/dist/src/views/iam/capabilities/yankCapability.js +6 -0
- package/dist/src/views/iam/capabilities/yankOptions.js +15 -0
- package/dist/src/views/iam/schema.js +7 -0
- package/dist/src/views/iam/types.js +1 -0
- package/dist/src/views/iam/utils.js +21 -0
- package/dist/src/views/route53/adapter.js +22 -0
- package/dist/src/views/s3/adapter.js +154 -0
- package/dist/src/views/s3/capabilities/actionCapability.js +172 -0
- package/dist/src/views/s3/capabilities/detailCapability.js +115 -0
- package/dist/src/views/s3/capabilities/editCapability.js +35 -0
- package/dist/src/views/s3/capabilities/yankCapability.js +6 -0
- package/dist/src/views/s3/capabilities/yankOptions.js +55 -0
- package/dist/src/views/s3/client.js +12 -0
- package/dist/src/views/s3/fetcher.js +86 -0
- package/dist/src/views/s3/schema.js +6 -0
- package/dist/src/views/s3/utils.js +19 -0
- package/dist/src/views/secretsmanager/adapter.js +188 -0
- package/dist/src/views/secretsmanager/capabilities/actionCapability.js +193 -0
- package/dist/src/views/secretsmanager/capabilities/detailCapability.js +46 -0
- package/dist/src/views/secretsmanager/capabilities/editCapability.js +116 -0
- package/dist/src/views/secretsmanager/capabilities/yankCapability.js +7 -0
- package/dist/src/views/secretsmanager/capabilities/yankOptions.js +68 -0
- package/dist/src/views/secretsmanager/schema.js +28 -0
- package/dist/src/views/secretsmanager/types.js +1 -0
- package/package.json +68 -5
- package/index.js +0 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { runAwsJsonAsync } from "../../../utils/aws.js";
|
|
2
|
+
import { readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
export function createSecretsManagerEditCapability(region, getLevel) {
|
|
6
|
+
const regionArgs = region ? ["--region", region] : [];
|
|
7
|
+
const onEdit = async (row) => {
|
|
8
|
+
const level = getLevel?.();
|
|
9
|
+
const meta = row.meta;
|
|
10
|
+
if (!meta) {
|
|
11
|
+
return { action: "none" };
|
|
12
|
+
}
|
|
13
|
+
// Level 1: open secret in editor (both JSON and non-JSON)
|
|
14
|
+
if (level?.kind === "secrets" && meta.type === "secret") {
|
|
15
|
+
// Fetch secret value
|
|
16
|
+
const secretData = await runAwsJsonAsync([
|
|
17
|
+
"secretsmanager",
|
|
18
|
+
"get-secret-value",
|
|
19
|
+
"--secret-id",
|
|
20
|
+
meta.arn,
|
|
21
|
+
...regionArgs,
|
|
22
|
+
]);
|
|
23
|
+
const secretString = secretData.SecretString || "";
|
|
24
|
+
const safeName = meta.name.replace(/[^a-z0-9_-]/gi, "_");
|
|
25
|
+
const filePath = join(tmpdir(), `a9s_secret_${safeName}`);
|
|
26
|
+
await writeFile(filePath, secretString, { mode: 0o600 });
|
|
27
|
+
return {
|
|
28
|
+
action: "edit",
|
|
29
|
+
filePath,
|
|
30
|
+
metadata: {
|
|
31
|
+
secretArn: meta.arn,
|
|
32
|
+
secretName: meta.name,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Level 2: field edits
|
|
37
|
+
if (level?.kind === "secret-fields" && meta.type === "secret-field") {
|
|
38
|
+
const fieldValue = meta.value || "";
|
|
39
|
+
const safeName = meta.key.replace(/[^a-z0-9_-]/gi, "_");
|
|
40
|
+
const filePath = join(tmpdir(), `a9s_field_${safeName}`);
|
|
41
|
+
await writeFile(filePath, fieldValue, { mode: 0o600 });
|
|
42
|
+
return {
|
|
43
|
+
action: "edit",
|
|
44
|
+
filePath,
|
|
45
|
+
metadata: {
|
|
46
|
+
fieldKey: meta.key,
|
|
47
|
+
secretArn: meta.secretArn,
|
|
48
|
+
secretName: meta.secretName,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return { action: "none" };
|
|
53
|
+
};
|
|
54
|
+
const uploadFile = async (filePath, metadata) => {
|
|
55
|
+
const fieldKey = metadata.fieldKey;
|
|
56
|
+
const secretArn = metadata.secretArn;
|
|
57
|
+
try {
|
|
58
|
+
// Level 2: Update field in JSON secret
|
|
59
|
+
if (fieldKey && secretArn) {
|
|
60
|
+
const newContent = await readFile(filePath, "utf-8");
|
|
61
|
+
// Fetch entire secret
|
|
62
|
+
const secretData = await runAwsJsonAsync([
|
|
63
|
+
"secretsmanager",
|
|
64
|
+
"get-secret-value",
|
|
65
|
+
"--secret-id",
|
|
66
|
+
secretArn,
|
|
67
|
+
...regionArgs,
|
|
68
|
+
]);
|
|
69
|
+
const secretString = secretData.SecretString || "";
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(secretString);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new Error("Failed to parse secret JSON");
|
|
76
|
+
}
|
|
77
|
+
// Update field
|
|
78
|
+
parsed[fieldKey] = newContent;
|
|
79
|
+
// Put secret back
|
|
80
|
+
const updatedSecretString = JSON.stringify(parsed);
|
|
81
|
+
await runAwsJsonAsync([
|
|
82
|
+
"secretsmanager",
|
|
83
|
+
"put-secret-value",
|
|
84
|
+
"--secret-id",
|
|
85
|
+
secretArn,
|
|
86
|
+
"--secret-string",
|
|
87
|
+
updatedSecretString,
|
|
88
|
+
...regionArgs,
|
|
89
|
+
]);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Level 1: Update entire secret (JSON or non-JSON)
|
|
93
|
+
if (secretArn && !fieldKey) {
|
|
94
|
+
const newContent = await readFile(filePath, "utf-8");
|
|
95
|
+
await runAwsJsonAsync([
|
|
96
|
+
"secretsmanager",
|
|
97
|
+
"put-secret-value",
|
|
98
|
+
"--secret-id",
|
|
99
|
+
secretArn,
|
|
100
|
+
"--secret-string",
|
|
101
|
+
newContent,
|
|
102
|
+
...regionArgs,
|
|
103
|
+
]);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
+
throw new Error(`Failed to update secret: ${message}`);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
onEdit,
|
|
114
|
+
uploadFile,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createYankCapability } from "../../../adapters/capabilities/YankCapability.js";
|
|
2
|
+
import { SecretRowMetaSchema } from "../schema.js";
|
|
3
|
+
import { secretYankOptions } from "./yankOptions.js";
|
|
4
|
+
export function createSecretsManagerYankCapability(region, getLevel) {
|
|
5
|
+
const ctx = { region, getLevel };
|
|
6
|
+
return createYankCapability(secretYankOptions, SecretRowMetaSchema, ctx);
|
|
7
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { runAwsJsonAsync } from "../../../utils/aws.js";
|
|
2
|
+
const isSecret = (row) => row.meta.type === "secret";
|
|
3
|
+
const isSecretField = (row) => row.meta.type === "secret-field";
|
|
4
|
+
export const secretYankOptions = [
|
|
5
|
+
{
|
|
6
|
+
trigger: { type: "key", char: "n" },
|
|
7
|
+
label: "copy name",
|
|
8
|
+
feedback: "Copied Secret Name",
|
|
9
|
+
headerKey: "name",
|
|
10
|
+
isRelevant: isSecret,
|
|
11
|
+
resolve: async (row) => {
|
|
12
|
+
return row.meta.name || null;
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
trigger: { type: "key", char: "a" },
|
|
17
|
+
label: "copy arn",
|
|
18
|
+
feedback: "Copied ARN",
|
|
19
|
+
headerKey: "name",
|
|
20
|
+
isRelevant: isSecret,
|
|
21
|
+
resolve: async (row) => {
|
|
22
|
+
return row.meta.arn || null;
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
trigger: { type: "key", char: "v" },
|
|
27
|
+
label: "copy secret value",
|
|
28
|
+
feedback: "Copied Secret Value",
|
|
29
|
+
headerKey: "name",
|
|
30
|
+
isRelevant: isSecret,
|
|
31
|
+
resolve: async (row, ctx) => {
|
|
32
|
+
const regionArgs = ctx.region ? ["--region", ctx.region] : [];
|
|
33
|
+
try {
|
|
34
|
+
const secretData = await runAwsJsonAsync([
|
|
35
|
+
"secretsmanager",
|
|
36
|
+
"get-secret-value",
|
|
37
|
+
"--secret-id",
|
|
38
|
+
row.meta.arn,
|
|
39
|
+
...regionArgs,
|
|
40
|
+
]);
|
|
41
|
+
return secretData.SecretString || null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
trigger: { type: "key", char: "k" },
|
|
50
|
+
label: "copy field key",
|
|
51
|
+
feedback: "Copied Field Key",
|
|
52
|
+
headerKey: "key",
|
|
53
|
+
isRelevant: isSecretField,
|
|
54
|
+
resolve: async (row) => {
|
|
55
|
+
return row.meta.key || null;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
trigger: { type: "key", char: "v" },
|
|
60
|
+
label: "copy field value",
|
|
61
|
+
feedback: "Copied Field Value",
|
|
62
|
+
headerKey: "value",
|
|
63
|
+
isRelevant: isSecretField,
|
|
64
|
+
resolve: async (row) => {
|
|
65
|
+
return row.meta.value || null;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Strict schemas for each row type
|
|
3
|
+
export const SecretMetaSchema = z.object({
|
|
4
|
+
type: z.literal("secret"),
|
|
5
|
+
name: z.string(),
|
|
6
|
+
arn: z.string(),
|
|
7
|
+
description: z.string().optional(),
|
|
8
|
+
});
|
|
9
|
+
export const SecretFieldMetaSchema = z.object({
|
|
10
|
+
type: z.literal("secret-field"),
|
|
11
|
+
key: z.string(),
|
|
12
|
+
value: z.string(),
|
|
13
|
+
secretArn: z.string(),
|
|
14
|
+
secretName: z.string(),
|
|
15
|
+
});
|
|
16
|
+
// Union schema for validation
|
|
17
|
+
export const SecretRowMetaUnionSchema = z.union([SecretMetaSchema, SecretFieldMetaSchema]);
|
|
18
|
+
// Flat schema for use with yank capability
|
|
19
|
+
export const SecretRowMetaSchema = z.object({
|
|
20
|
+
type: z.enum(["secret", "secret-field"]),
|
|
21
|
+
name: z.string().optional(),
|
|
22
|
+
arn: z.string().optional(),
|
|
23
|
+
description: z.string().optional(),
|
|
24
|
+
key: z.string().optional(),
|
|
25
|
+
value: z.string().optional(),
|
|
26
|
+
secretArn: z.string().optional(),
|
|
27
|
+
secretName: z.string().optional(),
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,9 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a9s/cli",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "k9s-style TUI navigator for AWS services",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"aws",
|
|
7
|
+
"cli",
|
|
8
|
+
"dynamodb",
|
|
9
|
+
"iam",
|
|
10
|
+
"k9s",
|
|
11
|
+
"route53",
|
|
12
|
+
"s3",
|
|
13
|
+
"tui"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/IamShobe/a9s",
|
|
5
16
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/IamShobe/a9s.git"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"a9s": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"localstack:up": "docker compose -f docker/docker-compose.yml up -d",
|
|
33
|
+
"localstack:down": "docker compose -f docker/docker-compose.yml down",
|
|
34
|
+
"localstack:seed": "tsx scripts/seed.ts",
|
|
35
|
+
"localstack:setup": "pnpm localstack:up && sleep 3 && pnpm localstack:seed",
|
|
36
|
+
"dev:local": "AWS_ENDPOINT_URL=http://localhost:4566 AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test tsx --watch src/index.tsx",
|
|
37
|
+
"dev": "tsx --watch src/index.tsx",
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"lint": "oxlint",
|
|
40
|
+
"format": "oxfmt --write",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"prepublishOnly": "pnpm build"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@aws-sdk/client-s3": "^3.1002.0",
|
|
47
|
+
"@commander-js/extra-typings": "^14.0.0",
|
|
48
|
+
"@inkjs/ui": "^2.0.0",
|
|
49
|
+
"clipboardy": "^5.3.1",
|
|
50
|
+
"commander": "^14.0.3",
|
|
51
|
+
"ink": "^6.8.0",
|
|
52
|
+
"ink-text-input": "^6.0.0",
|
|
53
|
+
"jotai": "^2.18.0",
|
|
54
|
+
"open": "^11.0.0",
|
|
55
|
+
"open-editor": "^6.0.0",
|
|
56
|
+
"react": "^19.2.4",
|
|
57
|
+
"zod": "^4.3.6"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^25.3.3",
|
|
61
|
+
"@types/react": "^19.2.14",
|
|
62
|
+
"ink-testing-library": "^4.0.0",
|
|
63
|
+
"oxfmt": "^0.36.0",
|
|
64
|
+
"oxlint": "^1.51.0",
|
|
65
|
+
"tsx": "^4.21.0",
|
|
66
|
+
"typescript": "^5.9.3",
|
|
67
|
+
"vitest": "^4.0.18"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=18"
|
|
71
|
+
}
|
|
9
72
|
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = {};
|