@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.
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 +68 -5
  95. package/index.js +0 -1
@@ -0,0 +1,35 @@
1
+ import { PutObjectCommand } from "@aws-sdk/client-s3";
2
+ import { readFile } from "fs/promises";
3
+ import { downloadObject } from "../fetcher.js";
4
+ export function createS3EditCapability(client, getLevel) {
5
+ const onEdit = async (row) => {
6
+ const level = getLevel();
7
+ const type = row.meta?.type;
8
+ if (level.kind !== "objects" || type !== "object") {
9
+ return { action: "none" };
10
+ }
11
+ const filePath = await downloadObject(client, level.bucket, row.meta?.key);
12
+ return {
13
+ action: "edit",
14
+ filePath,
15
+ metadata: {
16
+ bucket: level.bucket,
17
+ key: row.meta?.key,
18
+ },
19
+ };
20
+ };
21
+ const uploadFile = async (filePath, metadata) => {
22
+ const bucket = metadata.bucket;
23
+ const key = metadata.key;
24
+ const fileContent = await readFile(filePath);
25
+ await client.send(new PutObjectCommand({
26
+ Bucket: bucket,
27
+ Key: key,
28
+ Body: fileContent,
29
+ }));
30
+ };
31
+ return {
32
+ onEdit,
33
+ uploadFile,
34
+ };
35
+ }
@@ -0,0 +1,6 @@
1
+ import { createYankCapability } from "../../../adapters/capabilities/YankCapability.js";
2
+ import { S3RowMetaSchema } from "../schema.js";
3
+ import { s3YankOptions } from "./yankOptions.js";
4
+ export function createS3YankCapability(ctx) {
5
+ return createYankCapability(s3YankOptions, S3RowMetaSchema, ctx);
6
+ }
@@ -0,0 +1,55 @@
1
+ const isS3Navigable = (row) => row.meta.type === "bucket" || row.meta.type === "folder" || row.meta.type === "object";
2
+ export const s3YankOptions = [
3
+ {
4
+ trigger: { type: "key", char: "k" },
5
+ label: "copy key/path",
6
+ feedback: "Copied Key",
7
+ headerKey: "name",
8
+ isRelevant: isS3Navigable,
9
+ resolve: async (row, ctx) => {
10
+ const level = ctx.getLevel();
11
+ const bucket = level.kind === "objects" ? level.bucket : row.id;
12
+ if (row.meta.type === "bucket")
13
+ return `s3://${row.id}`;
14
+ return `s3://${bucket}/${row.meta.key}`;
15
+ },
16
+ },
17
+ {
18
+ trigger: { type: "key", char: "a" },
19
+ label: "copy arn",
20
+ feedback: "Copied ARN",
21
+ headerKey: "name",
22
+ isRelevant: isS3Navigable,
23
+ resolve: async (row, ctx) => {
24
+ const level = ctx.getLevel();
25
+ const bucket = level.kind === "objects" ? level.bucket : row.id;
26
+ if (row.meta.type === "bucket")
27
+ return `arn:aws:s3:::${row.id}`;
28
+ return `arn:aws:s3:::${bucket}/${row.meta.key}`;
29
+ },
30
+ },
31
+ {
32
+ trigger: { type: "key", char: "e" },
33
+ label: "copy etag",
34
+ feedback: "Copied ETag",
35
+ headerKey: "name",
36
+ isRelevant: (row) => row.meta.type === "object",
37
+ resolve: async (row, ctx) => {
38
+ const fields = await ctx.getDetails(row);
39
+ const etag = fields.find((f) => f.label === "ETag")?.value ?? "";
40
+ return etag && etag !== "-" ? etag : null;
41
+ },
42
+ },
43
+ {
44
+ trigger: { type: "key", char: "d" },
45
+ label: "copy last-modified",
46
+ feedback: "Copied Last Modified",
47
+ headerKey: "lastModified",
48
+ isRelevant: (row) => row.meta.type === "object",
49
+ resolve: async (row, ctx) => {
50
+ const fields = await ctx.getDetails(row);
51
+ const lastModified = fields.find((f) => f.label === "Last Modified")?.value ?? "";
52
+ return lastModified && lastModified !== "-" ? lastModified : null;
53
+ },
54
+ },
55
+ ];
@@ -0,0 +1,12 @@
1
+ import { S3Client } from "@aws-sdk/client-s3";
2
+ export function createS3Client(endpointUrl, region) {
3
+ return new S3Client({
4
+ ...(region ? { region } : {}),
5
+ ...(endpointUrl
6
+ ? {
7
+ endpoint: endpointUrl,
8
+ forcePathStyle: true,
9
+ }
10
+ : {}),
11
+ });
12
+ }
@@ -0,0 +1,86 @@
1
+ import { ListBucketsCommand, ListObjectsV2Command, GetObjectCommand, HeadObjectCommand, } from "@aws-sdk/client-s3";
2
+ import { mkdir, stat, writeFile } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import { basename, join, resolve } from "path";
5
+ export async function fetchBuckets(client) {
6
+ const response = await client.send(new ListBucketsCommand({}));
7
+ return (response.Buckets ?? []).map((b) => ({
8
+ name: b.Name ?? "",
9
+ creationDate: b.CreationDate,
10
+ }));
11
+ }
12
+ export async function fetchObjects(client, bucket, prefix) {
13
+ const response = await client.send(new ListObjectsV2Command({
14
+ Bucket: bucket,
15
+ Prefix: prefix || undefined,
16
+ Delimiter: "/",
17
+ }));
18
+ const folders = (response.CommonPrefixes ?? []).map((cp) => ({
19
+ key: cp.Prefix ?? "",
20
+ size: undefined,
21
+ lastModified: undefined,
22
+ isFolder: true,
23
+ }));
24
+ const files = (response.Contents ?? [])
25
+ .filter((obj) => obj.Key !== prefix) // skip the "directory" marker itself
26
+ .map((obj) => ({
27
+ key: obj.Key ?? "",
28
+ size: obj.Size,
29
+ lastModified: obj.LastModified ?? undefined,
30
+ isFolder: false,
31
+ }));
32
+ return [...folders, ...files];
33
+ }
34
+ export async function downloadObject(client, bucket, key) {
35
+ const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
36
+ if (!response.Body)
37
+ throw new Error("Empty response body");
38
+ const bytes = await response.Body.transformToByteArray();
39
+ const safeName = key.replace(/\//g, "_");
40
+ const filePath = join(tmpdir(), `a9s_${safeName}`);
41
+ await writeFile(filePath, bytes);
42
+ return filePath;
43
+ }
44
+ export async function downloadObjectToPath(client, bucket, key, destinationPath, overwrite = false) {
45
+ const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
46
+ if (!response.Body)
47
+ throw new Error("Empty response body");
48
+ const bytes = await response.Body.transformToByteArray();
49
+ const rawTarget = destinationPath.trim();
50
+ const absTarget = resolve(rawTarget);
51
+ const fileName = basename(key);
52
+ const endsWithSlash = rawTarget.endsWith("/") || rawTarget.endsWith("\\");
53
+ let finalPath = absTarget;
54
+ if (endsWithSlash) {
55
+ await mkdir(absTarget, { recursive: true });
56
+ finalPath = join(absTarget, fileName);
57
+ }
58
+ else {
59
+ const targetStat = await stat(absTarget).catch(() => null);
60
+ if (targetStat?.isDirectory()) {
61
+ finalPath = join(absTarget, fileName);
62
+ }
63
+ else if (targetStat?.isFile()) {
64
+ finalPath = absTarget;
65
+ if (!overwrite) {
66
+ throw new Error(`EEXIST_FILE:${finalPath}`);
67
+ }
68
+ }
69
+ }
70
+ await mkdir(resolve(finalPath, ".."), { recursive: true });
71
+ await writeFile(finalPath, bytes);
72
+ return finalPath;
73
+ }
74
+ export async function headObject(client, bucket, key) {
75
+ const res = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
76
+ return {
77
+ contentType: res.ContentType,
78
+ contentLength: res.ContentLength?.toString(),
79
+ etag: res.ETag?.replace(/"/g, ""),
80
+ lastModified: res.LastModified?.toISOString().replace("T", " ").slice(0, 19),
81
+ storageClass: res.StorageClass ?? "STANDARD",
82
+ versionId: res.VersionId,
83
+ serverSideEncryption: res.ServerSideEncryption,
84
+ ...Object.fromEntries(Object.entries(res.Metadata ?? {}).map(([k, v]) => [`meta:${k}`, v])),
85
+ };
86
+ }
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+ export const S3RowMetaSchema = z.discriminatedUnion("type", [
3
+ z.object({ type: z.literal("bucket") }),
4
+ z.object({ type: z.literal("folder"), key: z.string() }),
5
+ z.object({ type: z.literal("object"), key: z.string() }),
6
+ ]);
@@ -0,0 +1,19 @@
1
+ export function formatSize(bytes) {
2
+ if (bytes === undefined)
3
+ return "-";
4
+ if (bytes < 1024)
5
+ return `${bytes} B`;
6
+ if (bytes < 1024 * 1024)
7
+ return `${(bytes / 1024).toFixed(1)} KB`;
8
+ if (bytes < 1024 * 1024 * 1024)
9
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
10
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
11
+ }
12
+ export function toParentPrefix(spec) {
13
+ if (!spec)
14
+ return "";
15
+ if (spec.endsWith("/"))
16
+ return spec;
17
+ const idx = spec.lastIndexOf("/");
18
+ return idx >= 0 ? spec.slice(0, idx + 1) : "";
19
+ }
@@ -0,0 +1,188 @@
1
+ import { textCell, secretCell } from "../../types.js";
2
+ import { runAwsJsonAsync } from "../../utils/aws.js";
3
+ import { atom } from "jotai";
4
+ import { getDefaultStore } from "jotai";
5
+ import { revealSecretsAtom } from "../../state/atoms.js";
6
+ import { createSecretsManagerDetailCapability } from "./capabilities/detailCapability.js";
7
+ import { createSecretsManagerYankCapability } from "./capabilities/yankCapability.js";
8
+ import { createSecretsManagerActionCapability } from "./capabilities/actionCapability.js";
9
+ import { createSecretsManagerEditCapability } from "./capabilities/editCapability.js";
10
+ export const secretLevelAtom = atom({ kind: "secrets" });
11
+ export const secretBackStackAtom = atom([]);
12
+ function tryParseFields(secretString) {
13
+ try {
14
+ const parsed = JSON.parse(secretString);
15
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
16
+ return Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
17
+ }
18
+ }
19
+ catch {
20
+ // Not JSON
21
+ }
22
+ return null;
23
+ }
24
+ export function createSecretsManagerServiceAdapter(endpointUrl, region) {
25
+ const store = getDefaultStore();
26
+ const regionArgs = region ? ["--region", region] : [];
27
+ // Getters and setters for level/backStack from atoms
28
+ const getLevel = () => store.get(secretLevelAtom);
29
+ const setLevel = (level) => store.set(secretLevelAtom, level);
30
+ const getBackStack = () => store.get(secretBackStackAtom);
31
+ const setBackStack = (stack) => store.set(secretBackStackAtom, stack);
32
+ const setReveal = (reveal) => store.set(revealSecretsAtom, reveal);
33
+ const getColumns = () => {
34
+ const level = getLevel();
35
+ if (level.kind === "secrets") {
36
+ return [
37
+ { key: "name", label: "Name" },
38
+ { key: "description", label: "Description", width: 30 },
39
+ { key: "lastChanged", label: "Last Changed", width: 22 },
40
+ { key: "rotation", label: "Rotation", width: 10 },
41
+ ];
42
+ }
43
+ // secret-fields level
44
+ return [
45
+ { key: "key", label: "Key" },
46
+ { key: "value", label: "Value", width: 50 },
47
+ ];
48
+ };
49
+ const getRows = async () => {
50
+ const level = getLevel();
51
+ if (level.kind === "secrets") {
52
+ const data = await runAwsJsonAsync([
53
+ "secretsmanager",
54
+ "list-secrets",
55
+ ...regionArgs,
56
+ ]);
57
+ return (data.SecretList ?? []).map((secret) => ({
58
+ id: secret.ARN,
59
+ cells: {
60
+ name: textCell(secret.Name),
61
+ description: textCell(secret.Description ?? ""),
62
+ lastChanged: textCell(secret.LastChangedDate
63
+ ? new Date(secret.LastChangedDate).toISOString().replace("T", " ").slice(0, 19)
64
+ : "-"),
65
+ rotation: textCell(secret.RotationEnabled ? "✓" : "-"),
66
+ },
67
+ meta: {
68
+ type: "secret",
69
+ name: secret.Name,
70
+ arn: secret.ARN,
71
+ description: secret.Description ?? "",
72
+ },
73
+ }));
74
+ }
75
+ // secret-fields level
76
+ const { secretArn, secretName } = level;
77
+ try {
78
+ const secretData = await runAwsJsonAsync([
79
+ "secretsmanager",
80
+ "get-secret-value",
81
+ "--secret-id",
82
+ secretArn,
83
+ ...regionArgs,
84
+ ]);
85
+ const secretString = secretData.SecretString || "";
86
+ const fields = [];
87
+ // Always add $RAW field (the whole secret value)
88
+ fields.push({ key: "$RAW", value: secretString });
89
+ // If JSON, also add parsed fields
90
+ const parsed = tryParseFields(secretString);
91
+ if (parsed) {
92
+ Object.entries(parsed).forEach(([key, value]) => {
93
+ fields.push({ key, value });
94
+ });
95
+ }
96
+ // Store raw values; formatting happens at display time based on reveal state
97
+ return fields.map(({ key, value }) => ({
98
+ id: key,
99
+ cells: {
100
+ key: textCell(key),
101
+ value: secretCell(value), // Raw value - will be formatted at display time
102
+ },
103
+ meta: {
104
+ type: "secret-field",
105
+ key,
106
+ value, // Raw value stored in metadata too
107
+ secretArn,
108
+ secretName,
109
+ },
110
+ }));
111
+ }
112
+ catch {
113
+ return [];
114
+ }
115
+ };
116
+ const onSelect = async (row) => {
117
+ const level = getLevel();
118
+ const backStack = getBackStack();
119
+ const meta = row.meta;
120
+ if (level.kind === "secrets") {
121
+ if (!meta || meta.type !== "secret") {
122
+ return { action: "none" };
123
+ }
124
+ // Always navigate to level 2 (which includes $RAW field)
125
+ const newStack = [...backStack, { level: level, selectedIndex: 0 }];
126
+ setBackStack(newStack);
127
+ setLevel({
128
+ kind: "secret-fields",
129
+ secretArn: meta.arn,
130
+ secretName: meta.name,
131
+ });
132
+ // Reset reveal state when entering a secret (security: always start hidden)
133
+ setReveal(false);
134
+ return { action: "navigate" };
135
+ }
136
+ // secret-fields level: do nothing (use "e" key to edit)
137
+ return { action: "none" };
138
+ };
139
+ const canGoBack = () => getBackStack().length > 0;
140
+ const goBack = () => {
141
+ const backStack = getBackStack();
142
+ if (backStack.length > 0) {
143
+ const newStack = backStack.slice(0, -1);
144
+ const frame = backStack[backStack.length - 1];
145
+ setBackStack(newStack);
146
+ setLevel(frame.level);
147
+ }
148
+ };
149
+ const getPath = () => {
150
+ const level = getLevel();
151
+ if (level.kind === "secrets")
152
+ return "secrets://";
153
+ return `secrets://${level.secretName}`;
154
+ };
155
+ const getContextLabel = () => {
156
+ const level = getLevel();
157
+ if (level.kind === "secrets")
158
+ return "🔑 Secrets Manager";
159
+ return `🔑 Secret: ${level.secretName}`;
160
+ };
161
+ // Compose capabilities
162
+ const detailCapability = createSecretsManagerDetailCapability(region, getLevel);
163
+ const editCapability = createSecretsManagerEditCapability(region, getLevel);
164
+ const yankCapability = createSecretsManagerYankCapability(region, getLevel);
165
+ const actionCapability = createSecretsManagerActionCapability(region, getLevel);
166
+ return {
167
+ id: "secretsmanager",
168
+ label: "Secrets Manager",
169
+ hudColor: { bg: "blue", fg: "white" },
170
+ getColumns,
171
+ getRows,
172
+ onSelect,
173
+ canGoBack,
174
+ goBack,
175
+ getPath,
176
+ getContextLabel,
177
+ reset() {
178
+ setLevel({ kind: "secrets" });
179
+ setBackStack([]);
180
+ },
181
+ capabilities: {
182
+ edit: editCapability,
183
+ detail: detailCapability,
184
+ yank: yankCapability,
185
+ actions: actionCapability,
186
+ },
187
+ };
188
+ }
@@ -0,0 +1,193 @@
1
+ import { runAwsJsonAsync } from "../../../utils/aws.js";
2
+ import { writeFile, stat, mkdir } from "fs/promises";
3
+ import { resolve, join } from "path";
4
+ function toErrorMessage(error) {
5
+ if (error instanceof Error)
6
+ return error.message;
7
+ return String(error);
8
+ }
9
+ function hasCode(error, code) {
10
+ return Boolean(typeof error === "object" &&
11
+ error !== null &&
12
+ "code" in error &&
13
+ error.code === code);
14
+ }
15
+ export function createSecretsManagerActionCapability(region, getLevel) {
16
+ const regionArgs = region ? ["--region", region] : [];
17
+ const getKeybindings = () => [
18
+ {
19
+ trigger: { type: "key", char: "f" },
20
+ actionId: "fetch",
21
+ label: "Fetch / save to file",
22
+ shortLabel: "save file",
23
+ scope: "navigate",
24
+ adapterId: "secrets-manager",
25
+ },
26
+ ];
27
+ const executeAction = async (actionId, context) => {
28
+ const level = getLevel?.();
29
+ if (actionId === "fetch") {
30
+ // Start fetch flow: prompt for path
31
+ if (!context.row)
32
+ return { type: "error", message: "No row selected" };
33
+ const meta = context.row.meta;
34
+ if (!meta) {
35
+ return { type: "error", message: "No item selected" };
36
+ }
37
+ const isValidLevel = (level?.kind === "secrets" && meta.type === "secret") ||
38
+ (level?.kind === "secret-fields" && meta.type === "secret-field");
39
+ if (!isValidLevel) {
40
+ return { type: "error", message: "Fetch is not available for this item" };
41
+ }
42
+ const promptLabel = level?.kind === "secrets" ? "Fetch secret to:" : "Fetch field to:";
43
+ return {
44
+ type: "prompt",
45
+ label: promptLabel,
46
+ defaultValue: "",
47
+ nextActionId: "fetch:submit",
48
+ };
49
+ }
50
+ if (actionId === "fetch:submit") {
51
+ // User entered a path; try to write the secret/field
52
+ if (!context.row || !context.data?.path) {
53
+ return { type: "error", message: "Invalid path" };
54
+ }
55
+ const meta = context.row.meta;
56
+ if (!meta) {
57
+ return { type: "error", message: "No item selected" };
58
+ }
59
+ let content = "";
60
+ let defaultFileName = "data.txt";
61
+ try {
62
+ if (level?.kind === "secrets" && meta.type === "secret") {
63
+ // Fetch secret value
64
+ const secretData = await runAwsJsonAsync([
65
+ "secretsmanager",
66
+ "get-secret-value",
67
+ "--secret-id",
68
+ meta.arn,
69
+ ...regionArgs,
70
+ ]);
71
+ content =
72
+ secretData.SecretString ||
73
+ (secretData.SecretBinary
74
+ ? Buffer.from(secretData.SecretBinary, "base64").toString()
75
+ : "");
76
+ defaultFileName = `${meta.name}.txt`;
77
+ }
78
+ else if (level?.kind === "secret-fields" && meta.type === "secret-field") {
79
+ // Use field value directly
80
+ content = meta.value || "";
81
+ defaultFileName = `${meta.key}.txt`;
82
+ }
83
+ else {
84
+ return { type: "error", message: "Invalid item type" };
85
+ }
86
+ const destinationPath = context.data.path;
87
+ // Handle destination path
88
+ const rawTarget = destinationPath.trim();
89
+ const absTarget = resolve(rawTarget);
90
+ const endsWithSlash = rawTarget.endsWith("/") || rawTarget.endsWith("\\");
91
+ let finalPath = absTarget;
92
+ if (endsWithSlash) {
93
+ await mkdir(absTarget, { recursive: true });
94
+ finalPath = join(absTarget, defaultFileName);
95
+ }
96
+ else {
97
+ const targetStat = await stat(absTarget).catch(() => null);
98
+ if (targetStat?.isDirectory()) {
99
+ finalPath = join(absTarget, defaultFileName);
100
+ }
101
+ else if (targetStat?.isFile()) {
102
+ finalPath = absTarget;
103
+ throw new Error("EEXIST_FILE:" + finalPath);
104
+ }
105
+ }
106
+ // Ensure parent directory exists
107
+ await mkdir(resolve(finalPath, ".."), { recursive: true });
108
+ await writeFile(finalPath, content, { mode: 0o600 });
109
+ return {
110
+ type: "feedback",
111
+ message: `Saved to ${finalPath}`,
112
+ };
113
+ }
114
+ catch (err) {
115
+ // Check for file exists error
116
+ if (hasCode(err, "EEXIST")) {
117
+ const destinationPath = context.data.path;
118
+ return {
119
+ type: "confirm",
120
+ message: `File exists. Overwrite ${destinationPath}?`,
121
+ nextActionId: "fetch:overwrite",
122
+ data: { path: destinationPath },
123
+ };
124
+ }
125
+ if (err instanceof Error && err.message.includes("EEXIST_FILE")) {
126
+ const filePath = err.message.replace("EEXIST_FILE:", "");
127
+ return {
128
+ type: "confirm",
129
+ message: `File exists. Overwrite ${filePath}?`,
130
+ nextActionId: "fetch:overwrite",
131
+ data: { path: filePath },
132
+ };
133
+ }
134
+ return {
135
+ type: "error",
136
+ message: `Fetch failed: ${toErrorMessage(err)}`,
137
+ };
138
+ }
139
+ }
140
+ if (actionId === "fetch:overwrite") {
141
+ // User confirmed overwrite
142
+ if (!context.row || !context.data?.path) {
143
+ return { type: "error", message: "Invalid path" };
144
+ }
145
+ const meta = context.row.meta;
146
+ if (!meta) {
147
+ return { type: "error", message: "No item selected" };
148
+ }
149
+ const destinationPath = context.data.path;
150
+ try {
151
+ let content = "";
152
+ if (level?.kind === "secrets" && meta.type === "secret") {
153
+ // Fetch secret value
154
+ const secretData = await runAwsJsonAsync([
155
+ "secretsmanager",
156
+ "get-secret-value",
157
+ "--secret-id",
158
+ meta.arn,
159
+ ...regionArgs,
160
+ ]);
161
+ content =
162
+ secretData.SecretString ||
163
+ (secretData.SecretBinary
164
+ ? Buffer.from(secretData.SecretBinary, "base64").toString()
165
+ : "");
166
+ }
167
+ else if (level?.kind === "secret-fields" && meta.type === "secret-field") {
168
+ // Use field value directly
169
+ content = meta.value || "";
170
+ }
171
+ else {
172
+ return { type: "error", message: "Invalid item type" };
173
+ }
174
+ await writeFile(destinationPath, content, { mode: 0o600 });
175
+ return {
176
+ type: "feedback",
177
+ message: `Saved to ${destinationPath}`,
178
+ };
179
+ }
180
+ catch (err) {
181
+ return {
182
+ type: "error",
183
+ message: `Fetch failed: ${toErrorMessage(err)}`,
184
+ };
185
+ }
186
+ }
187
+ return { type: "none" };
188
+ };
189
+ return {
190
+ getKeybindings,
191
+ executeAction,
192
+ };
193
+ }
@@ -0,0 +1,46 @@
1
+ import { runAwsJsonAsync } from "../../../utils/aws.js";
2
+ export function createSecretsManagerDetailCapability(region, getLevel) {
3
+ const regionArgs = region ? ["--region", region] : [];
4
+ const getDetails = async (row) => {
5
+ const meta = row.meta;
6
+ if (!meta) {
7
+ return [];
8
+ }
9
+ const level = getLevel?.();
10
+ // Level 1: Secret details
11
+ if (level?.kind === "secrets" && meta.type === "secret") {
12
+ const data = await runAwsJsonAsync([
13
+ "secretsmanager",
14
+ "describe-secret",
15
+ "--secret-id",
16
+ meta.arn,
17
+ ...regionArgs,
18
+ ]);
19
+ const fields = [
20
+ { label: "Name", value: String(data.Name ?? "-") },
21
+ { label: "ARN", value: String(data.ARN ?? "-") },
22
+ { label: "Description", value: String(data.Description ?? "-") },
23
+ { label: "Last Changed", value: String(data.LastChangedDate ?? "-") },
24
+ { label: "Last Rotated", value: String(data.LastRotatedDate ?? "-") },
25
+ { label: "Rotation Enabled", value: data.RotationEnabled ? "Yes" : "No" },
26
+ { label: "KMS Key ID", value: String(data.KmsKeyId ?? "-") },
27
+ {
28
+ label: "Tags",
29
+ value: data.Tags && data.Tags.length > 0 ? `${data.Tags.length} tag(s)` : "-",
30
+ },
31
+ ];
32
+ return fields;
33
+ }
34
+ // Level 2: Field details
35
+ if (level?.kind === "secret-fields" && meta.type === "secret-field") {
36
+ const fields = [
37
+ { label: "Field Key", value: String(meta.key ?? "-") },
38
+ { label: "Secret Name", value: String(meta.secretName ?? "-") },
39
+ { label: "Value", value: String(meta.value ?? "-") },
40
+ ];
41
+ return fields;
42
+ }
43
+ return [];
44
+ };
45
+ return { getDetails };
46
+ }