@a9s/cli 1.0.10 → 1.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 (40) hide show
  1. package/dist/src/App.js +30 -12
  2. package/dist/src/adapters/backStackUtils.js +14 -0
  3. package/dist/src/components/DetailPanel.js +3 -5
  4. package/dist/src/components/DiffViewer.js +3 -5
  5. package/dist/src/components/ErrorStatePanel.js +1 -1
  6. package/dist/src/components/HUD.js +2 -2
  7. package/dist/src/components/HelpPanel.js +3 -9
  8. package/dist/src/components/Table/index.js +1 -14
  9. package/dist/src/components/TableSkeleton.js +1 -5
  10. package/dist/src/components/YankHelpPanel.js +13 -4
  11. package/dist/src/features/AppMainView.integration.test.js +1 -1
  12. package/dist/src/features/AppMainView.js +4 -4
  13. package/dist/src/hooks/useAppController.js +7 -29
  14. package/dist/src/hooks/useAppData.js +2 -9
  15. package/dist/src/hooks/useHelpPanel.js +8 -4
  16. package/dist/src/hooks/usePickerManager.js +1 -9
  17. package/dist/src/hooks/usePickerTable.js +2 -9
  18. package/dist/src/hooks/useTimedFeedback.js +31 -0
  19. package/dist/src/hooks/useYankMode.js +5 -14
  20. package/dist/src/utils/aws.js +4 -0
  21. package/dist/src/utils/errorHelpers.js +11 -0
  22. package/dist/src/utils/rowUtils.js +10 -0
  23. package/dist/src/utils/scrollUtils.js +11 -0
  24. package/dist/src/utils/textUtils.js +16 -0
  25. package/dist/src/views/dynamodb/adapter.js +5 -13
  26. package/dist/src/views/dynamodb/capabilities/detailCapability.js +2 -2
  27. package/dist/src/views/dynamodb/capabilities/yankCapability.js +1 -1
  28. package/dist/src/views/iam/adapter.js +28 -23
  29. package/dist/src/views/route53/adapter.js +5 -13
  30. package/dist/src/views/route53/capabilities/detailCapability.js +2 -2
  31. package/dist/src/views/route53/capabilities/yankCapability.js +1 -1
  32. package/dist/src/views/s3/adapter.js +2 -10
  33. package/dist/src/views/s3/capabilities/actionCapability.js +1 -11
  34. package/dist/src/views/secretsmanager/adapter.js +6 -19
  35. package/dist/src/views/secretsmanager/capabilities/actionCapability.js +4 -35
  36. package/dist/src/views/secretsmanager/capabilities/detailCapability.js +2 -9
  37. package/dist/src/views/secretsmanager/capabilities/editCapability.js +5 -39
  38. package/dist/src/views/secretsmanager/capabilities/yankOptions.js +2 -9
  39. package/dist/src/views/secretsmanager/client.js +31 -0
  40. package/package.json +1 -1
@@ -0,0 +1,16 @@
1
+ /** Truncate a string to maxLen, padding with spaces if shorter. */
2
+ export function truncate(str, maxLen) {
3
+ if (str.length <= maxLen)
4
+ return str.padEnd(maxLen);
5
+ return str.slice(0, maxLen - 1) + "…";
6
+ }
7
+ /** Truncate a string to maxLen without padding. */
8
+ export function truncateNoPad(str, maxLen) {
9
+ if (maxLen <= 0)
10
+ return "";
11
+ if (str.length <= maxLen)
12
+ return str;
13
+ if (maxLen === 1)
14
+ return "…";
15
+ return str.slice(0, maxLen - 1) + "…";
16
+ }
@@ -1,5 +1,6 @@
1
1
  import { textCell } from "../../types.js";
2
- import { runAwsJsonAsync } from "../../utils/aws.js";
2
+ import { runAwsJsonAsync, buildRegionArgs } from "../../utils/aws.js";
3
+ import { createBackStackHelpers } from "../../adapters/backStackUtils.js";
3
4
  import { atom } from "jotai";
4
5
  import { getDefaultStore } from "jotai";
5
6
  import { unwrapDynamoValue, formatBillingMode, formatDynamoValue, getDynamoType, extractPkValue, extractSkValue, } from "./utils.js";
@@ -14,7 +15,7 @@ const tableDescriptionCache = new Map();
14
15
  const itemsCache = new Map();
15
16
  export function createDynamoDBServiceAdapter(endpointUrl, region) {
16
17
  const store = getDefaultStore();
17
- const regionArgs = region ? ["--region", region] : [];
18
+ const regionArgs = buildRegionArgs(region);
18
19
  const getLevel = () => store.get(dynamoDBLevelAtom);
19
20
  const setLevel = (level) => store.set(dynamoDBLevelAtom, level);
20
21
  const getBackStack = () => store.get(dynamoDBBackStackAtom);
@@ -272,16 +273,7 @@ export function createDynamoDBServiceAdapter(endpointUrl, region) {
272
273
  // item-fields level: leaf, no drill-down
273
274
  return { action: "none" };
274
275
  };
275
- const canGoBack = () => getBackStack().length > 0;
276
- const goBack = () => {
277
- const backStack = getBackStack();
278
- if (backStack.length > 0) {
279
- const newStack = backStack.slice(0, -1);
280
- const frame = backStack[backStack.length - 1];
281
- setBackStack(newStack);
282
- setLevel(frame.level);
283
- }
284
- };
276
+ const { canGoBack, goBack } = createBackStackHelpers(getLevel, setLevel, getBackStack, setBackStack);
285
277
  const getPath = () => {
286
278
  const level = getLevel();
287
279
  if (level.kind === "tables")
@@ -300,7 +292,7 @@ export function createDynamoDBServiceAdapter(endpointUrl, region) {
300
292
  };
301
293
  // Compose capabilities
302
294
  const detailCapability = createDynamoDBDetailCapability(region, getLevel);
303
- const yankCapability = createDynamoDBYankCapability(region, getLevel);
295
+ const yankCapability = createDynamoDBYankCapability();
304
296
  return {
305
297
  id: "dynamodb",
306
298
  label: "DynamoDB",
@@ -1,7 +1,7 @@
1
- import { runAwsJsonAsync } from "../../../utils/aws.js";
1
+ import { runAwsJsonAsync, buildRegionArgs } from "../../../utils/aws.js";
2
2
  import { formatBillingMode, formatKeySchema } from "../utils.js";
3
3
  export function createDynamoDBDetailCapability(region, getLevel) {
4
- const regionArgs = region ? ["--region", region] : [];
4
+ const regionArgs = buildRegionArgs(region);
5
5
  const getDetails = async (row) => {
6
6
  const meta = row.meta;
7
7
  if (!meta) {
@@ -1,6 +1,6 @@
1
1
  import { createYankCapability } from "../../../adapters/capabilities/YankCapability.js";
2
2
  import { DynamoDBRowMetaSchema } from "../schema.js";
3
3
  import { DynamoDBYankOptions } from "./yankOptions.js";
4
- export function createDynamoDBYankCapability(_region, _getLevel) {
4
+ export function createDynamoDBYankCapability() {
5
5
  return createYankCapability(DynamoDBYankOptions, DynamoDBRowMetaSchema, {});
6
6
  }
@@ -1,25 +1,26 @@
1
1
  import { runAwsJsonAsync } from "../../utils/aws.js";
2
2
  import { textCell } from "../../types.js";
3
+ import { atom } from "jotai";
4
+ import { getDefaultStore } from "jotai";
3
5
  import { formatDate } from "./utils.js";
4
6
  import { createIamEditCapability } from "./capabilities/editCapability.js";
5
7
  import { createIamDetailCapability } from "./capabilities/detailCapability.js";
6
8
  import { createIamYankCapability } from "./capabilities/yankCapability.js";
7
9
  import { SERVICE_COLORS } from "../../constants/theme.js";
10
+ import { createBackStackHelpers } from "../../adapters/backStackUtils.js";
11
+ export const iamLevelAtom = atom({ kind: "root" });
12
+ export const iamBackStackAtom = atom([]);
8
13
  function getIamMeta(row) {
9
14
  return row.meta;
10
15
  }
11
16
  export function createIamServiceAdapter() {
12
- let level = { kind: "root" };
13
- let backStack = [];
14
- const getLevel = () => level;
15
- const setLevel = (newLevel) => {
16
- level = newLevel;
17
- };
18
- const getBackStack = () => backStack;
19
- const setBackStack = (newStack) => {
20
- backStack = newStack;
21
- };
17
+ const store = getDefaultStore();
18
+ const getLevel = () => store.get(iamLevelAtom);
19
+ const setLevel = (newLevel) => store.set(iamLevelAtom, newLevel);
20
+ const getBackStack = () => store.get(iamBackStackAtom);
21
+ const setBackStack = (newStack) => store.set(iamBackStackAtom, newStack);
22
22
  const getColumns = () => {
23
+ const level = getLevel();
23
24
  switch (level.kind) {
24
25
  case "root":
25
26
  case "role-menu":
@@ -44,6 +45,7 @@ export function createIamServiceAdapter() {
44
45
  }
45
46
  };
46
47
  const getRows = async () => {
48
+ const level = getLevel();
47
49
  switch (level.kind) {
48
50
  case "root":
49
51
  return [
@@ -70,19 +72,20 @@ export function createIamServiceAdapter() {
70
72
  meta: { type: "role", roleName: role.RoleName, arn: role.Arn },
71
73
  }));
72
74
  }
73
- case "role-menu":
75
+ case "role-menu": {
76
+ const { roleName } = level;
74
77
  return [
75
78
  {
76
- id: `${level.roleName}::inline`,
79
+ id: `${roleName}::inline`,
77
80
  cells: { name: textCell("Inline Policies"), type: textCell("Role Inline Policies") },
78
81
  meta: {
79
82
  type: "menu",
80
83
  kind: "role-inline-policies",
81
- roleName: level.roleName,
84
+ roleName,
82
85
  },
83
86
  },
84
87
  {
85
- id: `${level.roleName}::attached`,
88
+ id: `${roleName}::attached`,
86
89
  cells: {
87
90
  name: textCell("Attached Policies"),
88
91
  type: textCell("Role Attached Policies"),
@@ -90,10 +93,11 @@ export function createIamServiceAdapter() {
90
93
  meta: {
91
94
  type: "menu",
92
95
  kind: "role-attached-policies",
93
- roleName: level.roleName,
96
+ roleName,
94
97
  },
95
98
  },
96
99
  ];
100
+ }
97
101
  case "role-inline-policies": {
98
102
  const { roleName } = level;
99
103
  const data = await runAwsJsonAsync([
@@ -162,6 +166,8 @@ export function createIamServiceAdapter() {
162
166
  }
163
167
  };
164
168
  const onSelect = async (row) => {
169
+ const level = getLevel();
170
+ const backStack = getBackStack();
165
171
  const nextBackStack = [...backStack, { level, selectedIndex: 0 }];
166
172
  const meta = getIamMeta(row);
167
173
  if (level.kind === "root" && meta?.type === "menu" && meta.kind === "roles") {
@@ -195,15 +201,9 @@ export function createIamServiceAdapter() {
195
201
  }
196
202
  return { action: "none" };
197
203
  };
198
- const canGoBack = () => getBackStack().length > 0;
199
- const goBack = () => {
200
- const frame = getBackStack()[getBackStack().length - 1];
201
- if (!frame)
202
- return;
203
- setBackStack(getBackStack().slice(0, -1));
204
- setLevel(frame.level);
205
- };
204
+ const { canGoBack, goBack } = createBackStackHelpers(getLevel, setLevel, getBackStack, setBackStack);
206
205
  const getPath = () => {
206
+ const level = getLevel();
207
207
  switch (level.kind) {
208
208
  case "root":
209
209
  return "iam://";
@@ -220,6 +220,7 @@ export function createIamServiceAdapter() {
220
220
  }
221
221
  };
222
222
  const getContextLabel = () => {
223
+ const level = getLevel();
223
224
  switch (level.kind) {
224
225
  case "root":
225
226
  return "🔐 IAM Resources";
@@ -250,6 +251,10 @@ export function createIamServiceAdapter() {
250
251
  goBack,
251
252
  getPath,
252
253
  getContextLabel,
254
+ reset() {
255
+ setLevel({ kind: "root" });
256
+ setBackStack([]);
257
+ },
253
258
  capabilities: {
254
259
  edit: editCapability,
255
260
  detail: detailCapability,
@@ -1,5 +1,6 @@
1
1
  import { textCell } from "../../types.js";
2
- import { runAwsJsonAsync } from "../../utils/aws.js";
2
+ import { runAwsJsonAsync, buildRegionArgs } from "../../utils/aws.js";
3
+ import { createBackStackHelpers } from "../../adapters/backStackUtils.js";
3
4
  import { atom } from "jotai";
4
5
  import { getDefaultStore } from "jotai";
5
6
  import { createRoute53DetailCapability } from "./capabilities/detailCapability.js";
@@ -9,7 +10,7 @@ export const route53LevelAtom = atom({ kind: "zones" });
9
10
  export const route53BackStackAtom = atom([]);
10
11
  export function createRoute53ServiceAdapter(endpointUrl, region) {
11
12
  const store = getDefaultStore();
12
- const regionArgs = region ? ["--region", region] : [];
13
+ const regionArgs = buildRegionArgs(region);
13
14
  const getLevel = () => store.get(route53LevelAtom);
14
15
  const setLevel = (level) => store.set(route53LevelAtom, level);
15
16
  const getBackStack = () => store.get(route53BackStackAtom);
@@ -131,16 +132,7 @@ export function createRoute53ServiceAdapter(endpointUrl, region) {
131
132
  // records level: leaf, no drill-down
132
133
  return { action: "none" };
133
134
  };
134
- const canGoBack = () => getBackStack().length > 0;
135
- const goBack = () => {
136
- const backStack = getBackStack();
137
- if (backStack.length > 0) {
138
- const newStack = backStack.slice(0, -1);
139
- const frame = backStack[backStack.length - 1];
140
- setBackStack(newStack);
141
- setLevel(frame.level);
142
- }
143
- };
135
+ const { canGoBack, goBack } = createBackStackHelpers(getLevel, setLevel, getBackStack, setBackStack);
144
136
  const getPath = () => {
145
137
  const level = getLevel();
146
138
  if (level.kind === "zones")
@@ -155,7 +147,7 @@ export function createRoute53ServiceAdapter(endpointUrl, region) {
155
147
  };
156
148
  // Compose capabilities
157
149
  const detailCapability = createRoute53DetailCapability(region, getLevel);
158
- const yankCapability = createRoute53YankCapability(region, getLevel);
150
+ const yankCapability = createRoute53YankCapability();
159
151
  return {
160
152
  id: "route53",
161
153
  label: "Route53",
@@ -1,6 +1,6 @@
1
- import { runAwsJsonAsync } from "../../../utils/aws.js";
1
+ import { runAwsJsonAsync, buildRegionArgs } from "../../../utils/aws.js";
2
2
  export function createRoute53DetailCapability(region, getLevel) {
3
- const regionArgs = region ? ["--region", region] : [];
3
+ const regionArgs = buildRegionArgs(region);
4
4
  const getDetails = async (row) => {
5
5
  const meta = row.meta;
6
6
  if (!meta) {
@@ -1,6 +1,6 @@
1
1
  import { createYankCapability } from "../../../adapters/capabilities/YankCapability.js";
2
2
  import { Route53RowMetaSchema } from "../schema.js";
3
3
  import { Route53YankOptions } from "./yankOptions.js";
4
- export function createRoute53YankCapability(_region, _getLevel) {
4
+ export function createRoute53YankCapability() {
5
5
  return createYankCapability(Route53YankOptions, Route53RowMetaSchema, {});
6
6
  }
@@ -3,6 +3,7 @@ import { createS3Client } from "./client.js";
3
3
  import { fetchBuckets, fetchObjects, downloadObject } from "./fetcher.js";
4
4
  import { atom } from "jotai";
5
5
  import { getDefaultStore } from "jotai";
6
+ import { createBackStackHelpers } from "../../adapters/backStackUtils.js";
6
7
  import { formatSize } from "./utils.js";
7
8
  import { createS3EditCapability } from "./capabilities/editCapability.js";
8
9
  import { createS3DetailCapability } from "./capabilities/detailCapability.js";
@@ -98,16 +99,7 @@ export function createS3ServiceAdapter(endpointUrl, region) {
98
99
  }
99
100
  return { action: "none" };
100
101
  };
101
- const canGoBack = () => getBackStack().length > 0;
102
- const goBack = () => {
103
- const backStack = getBackStack();
104
- if (backStack.length > 0) {
105
- const newStack = backStack.slice(0, -1);
106
- const frame = backStack[backStack.length - 1];
107
- setBackStack(newStack);
108
- setLevel(frame.level);
109
- }
110
- };
102
+ const { canGoBack, goBack } = createBackStackHelpers(getLevel, setLevel, getBackStack, setBackStack);
111
103
  const getPath = () => {
112
104
  const level = getLevel();
113
105
  if (level.kind === "buckets")
@@ -1,16 +1,6 @@
1
1
  import { downloadObjectToPath } from "../fetcher.js";
2
2
  import { toParentPrefix } from "../utils.js";
3
- function toErrorMessage(error) {
4
- if (error instanceof Error)
5
- return error.message;
6
- return String(error);
7
- }
8
- function hasCode(error, code) {
9
- return Boolean(typeof error === "object" &&
10
- error !== null &&
11
- "code" in error &&
12
- error.code === code);
13
- }
3
+ import { toErrorMessage, hasCode } from "../../../utils/errorHelpers.js";
14
4
  export function createS3ActionCapability(client, getLevel, getBackStack, setBackStack, setLevel) {
15
5
  const getKeybindings = () => [
16
6
  {
@@ -1,5 +1,5 @@
1
1
  import { textCell, secretCell } from "../../types.js";
2
- import { runAwsJsonAsync } from "../../utils/aws.js";
2
+ import { runAwsJsonAsync, buildRegionArgs } from "../../utils/aws.js";
3
3
  import { atom } from "jotai";
4
4
  import { getDefaultStore } from "jotai";
5
5
  import { revealSecretsAtom } from "../../state/atoms.js";
@@ -7,6 +7,8 @@ import { createSecretsManagerDetailCapability } from "./capabilities/detailCapab
7
7
  import { createSecretsManagerYankCapability } from "./capabilities/yankCapability.js";
8
8
  import { createSecretsManagerActionCapability } from "./capabilities/actionCapability.js";
9
9
  import { createSecretsManagerEditCapability } from "./capabilities/editCapability.js";
10
+ import { getSecretValue } from "./client.js";
11
+ import { createBackStackHelpers } from "../../adapters/backStackUtils.js";
10
12
  import { SERVICE_COLORS } from "../../constants/theme.js";
11
13
  export const secretLevelAtom = atom({ kind: "secrets" });
12
14
  export const secretBackStackAtom = atom([]);
@@ -24,7 +26,7 @@ function tryParseFields(secretString) {
24
26
  }
25
27
  export function createSecretsManagerServiceAdapter(endpointUrl, region) {
26
28
  const store = getDefaultStore();
27
- const regionArgs = region ? ["--region", region] : [];
29
+ const regionArgs = buildRegionArgs(region);
28
30
  // Getters and setters for level/backStack from atoms
29
31
  const getLevel = () => store.get(secretLevelAtom);
30
32
  const setLevel = (level) => store.set(secretLevelAtom, level);
@@ -76,13 +78,7 @@ export function createSecretsManagerServiceAdapter(endpointUrl, region) {
76
78
  // secret-fields level
77
79
  const { secretArn, secretName } = level;
78
80
  try {
79
- const secretData = await runAwsJsonAsync([
80
- "secretsmanager",
81
- "get-secret-value",
82
- "--secret-id",
83
- secretArn,
84
- ...regionArgs,
85
- ]);
81
+ const secretData = await getSecretValue(secretArn, region);
86
82
  const secretString = secretData.SecretString || "";
87
83
  const fields = [];
88
84
  // Always add $RAW field (the whole secret value)
@@ -137,16 +133,7 @@ export function createSecretsManagerServiceAdapter(endpointUrl, region) {
137
133
  // secret-fields level: do nothing (use "e" key to edit)
138
134
  return { action: "none" };
139
135
  };
140
- const canGoBack = () => getBackStack().length > 0;
141
- const goBack = () => {
142
- const backStack = getBackStack();
143
- if (backStack.length > 0) {
144
- const newStack = backStack.slice(0, -1);
145
- const frame = backStack[backStack.length - 1];
146
- setBackStack(newStack);
147
- setLevel(frame.level);
148
- }
149
- };
136
+ const { canGoBack, goBack } = createBackStackHelpers(getLevel, setLevel, getBackStack, setBackStack);
150
137
  const getPath = () => {
151
138
  const level = getLevel();
152
139
  if (level.kind === "secrets")
@@ -1,19 +1,8 @@
1
- import { runAwsJsonAsync } from "../../../utils/aws.js";
1
+ import { toErrorMessage, hasCode } from "../../../utils/errorHelpers.js";
2
+ import { getSecretValue } from "../client.js";
2
3
  import { writeFile, stat, mkdir } from "fs/promises";
3
4
  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
5
  export function createSecretsManagerActionCapability(region, getLevel) {
16
- const regionArgs = region ? ["--region", region] : [];
17
6
  const getKeybindings = () => [
18
7
  {
19
8
  trigger: { type: "key", char: "f" },
@@ -60,14 +49,7 @@ export function createSecretsManagerActionCapability(region, getLevel) {
60
49
  let defaultFileName = "data.txt";
61
50
  try {
62
51
  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
- ]);
52
+ const secretData = await getSecretValue(meta.arn, region);
71
53
  content =
72
54
  secretData.SecretString ||
73
55
  (secretData.SecretBinary
@@ -76,7 +58,6 @@ export function createSecretsManagerActionCapability(region, getLevel) {
76
58
  defaultFileName = `${meta.name}.txt`;
77
59
  }
78
60
  else if (level?.kind === "secret-fields" && meta.type === "secret-field") {
79
- // Use field value directly
80
61
  content = meta.value || "";
81
62
  defaultFileName = `${meta.key}.txt`;
82
63
  }
@@ -84,7 +65,6 @@ export function createSecretsManagerActionCapability(region, getLevel) {
84
65
  return { type: "error", message: "Invalid item type" };
85
66
  }
86
67
  const destinationPath = context.data.path;
87
- // Handle destination path
88
68
  const rawTarget = destinationPath.trim();
89
69
  const absTarget = resolve(rawTarget);
90
70
  const endsWithSlash = rawTarget.endsWith("/") || rawTarget.endsWith("\\");
@@ -103,7 +83,6 @@ export function createSecretsManagerActionCapability(region, getLevel) {
103
83
  throw new Error("EEXIST_FILE:" + finalPath);
104
84
  }
105
85
  }
106
- // Ensure parent directory exists
107
86
  await mkdir(resolve(finalPath, ".."), { recursive: true });
108
87
  await writeFile(finalPath, content, { mode: 0o600 });
109
88
  return {
@@ -112,7 +91,6 @@ export function createSecretsManagerActionCapability(region, getLevel) {
112
91
  };
113
92
  }
114
93
  catch (err) {
115
- // Check for file exists error
116
94
  if (hasCode(err, "EEXIST")) {
117
95
  const destinationPath = context.data.path;
118
96
  return {
@@ -138,7 +116,6 @@ export function createSecretsManagerActionCapability(region, getLevel) {
138
116
  }
139
117
  }
140
118
  if (actionId === "fetch:overwrite") {
141
- // User confirmed overwrite
142
119
  if (!context.row || !context.data?.path) {
143
120
  return { type: "error", message: "Invalid path" };
144
121
  }
@@ -150,14 +127,7 @@ export function createSecretsManagerActionCapability(region, getLevel) {
150
127
  try {
151
128
  let content = "";
152
129
  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
- ]);
130
+ const secretData = await getSecretValue(meta.arn, region);
161
131
  content =
162
132
  secretData.SecretString ||
163
133
  (secretData.SecretBinary
@@ -165,7 +135,6 @@ export function createSecretsManagerActionCapability(region, getLevel) {
165
135
  : "");
166
136
  }
167
137
  else if (level?.kind === "secret-fields" && meta.type === "secret-field") {
168
- // Use field value directly
169
138
  content = meta.value || "";
170
139
  }
171
140
  else {
@@ -1,6 +1,5 @@
1
- import { runAwsJsonAsync } from "../../../utils/aws.js";
1
+ import { describeSecret } from "../client.js";
2
2
  export function createSecretsManagerDetailCapability(region, getLevel) {
3
- const regionArgs = region ? ["--region", region] : [];
4
3
  const getDetails = async (row) => {
5
4
  const meta = row.meta;
6
5
  if (!meta) {
@@ -9,13 +8,7 @@ export function createSecretsManagerDetailCapability(region, getLevel) {
9
8
  const level = getLevel?.();
10
9
  // Level 1: Secret details
11
10
  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
- ]);
11
+ const data = await describeSecret(meta.arn, region);
19
12
  const fields = [
20
13
  { label: "Name", value: String(data.Name ?? "-") },
21
14
  { label: "ARN", value: String(data.ARN ?? "-") },
@@ -1,9 +1,8 @@
1
- import { runAwsJsonAsync } from "../../../utils/aws.js";
2
1
  import { readFile, writeFile } from "fs/promises";
3
2
  import { tmpdir } from "os";
4
3
  import { join } from "path";
4
+ import { getSecretValue, putSecretValue } from "../client.js";
5
5
  export function createSecretsManagerEditCapability(region, getLevel) {
6
- const regionArgs = region ? ["--region", region] : [];
7
6
  const onEdit = async (row) => {
8
7
  const level = getLevel?.();
9
8
  const meta = row.meta;
@@ -12,14 +11,7 @@ export function createSecretsManagerEditCapability(region, getLevel) {
12
11
  }
13
12
  // Level 1: open secret in editor (both JSON and non-JSON)
14
13
  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
- ]);
14
+ const secretData = await getSecretValue(meta.arn, region);
23
15
  const secretString = secretData.SecretString || "";
24
16
  const safeName = meta.name.replace(/[^a-z0-9_-]/gi, "_");
25
17
  const filePath = join(tmpdir(), `a9s_secret_${safeName}`);
@@ -58,14 +50,7 @@ export function createSecretsManagerEditCapability(region, getLevel) {
58
50
  // Level 2: Update field in JSON secret
59
51
  if (fieldKey && secretArn) {
60
52
  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
- ]);
53
+ const secretData = await getSecretValue(secretArn, region);
69
54
  const secretString = secretData.SecretString || "";
70
55
  let parsed;
71
56
  try {
@@ -74,33 +59,14 @@ export function createSecretsManagerEditCapability(region, getLevel) {
74
59
  catch {
75
60
  throw new Error("Failed to parse secret JSON");
76
61
  }
77
- // Update field
78
62
  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
- ]);
63
+ await putSecretValue(secretArn, JSON.stringify(parsed), region);
90
64
  return;
91
65
  }
92
66
  // Level 1: Update entire secret (JSON or non-JSON)
93
67
  if (secretArn && !fieldKey) {
94
68
  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
- ]);
69
+ await putSecretValue(secretArn, newContent, region);
104
70
  return;
105
71
  }
106
72
  }
@@ -1,4 +1,4 @@
1
- import { runAwsJsonAsync } from "../../../utils/aws.js";
1
+ import { getSecretValue } from "../client.js";
2
2
  const isSecret = (row) => row.meta.type === "secret";
3
3
  const isSecretField = (row) => row.meta.type === "secret-field";
4
4
  export const secretYankOptions = [
@@ -29,15 +29,8 @@ export const secretYankOptions = [
29
29
  headerKey: "name",
30
30
  isRelevant: isSecret,
31
31
  resolve: async (row, ctx) => {
32
- const regionArgs = ctx.region ? ["--region", ctx.region] : [];
33
32
  try {
34
- const secretData = await runAwsJsonAsync([
35
- "secretsmanager",
36
- "get-secret-value",
37
- "--secret-id",
38
- row.meta.arn,
39
- ...regionArgs,
40
- ]);
33
+ const secretData = await getSecretValue(row.meta.arn, ctx.region);
41
34
  return secretData.SecretString || null;
42
35
  }
43
36
  catch {
@@ -0,0 +1,31 @@
1
+ import { runAwsJsonAsync } from "../../utils/aws.js";
2
+ import { buildRegionArgs } from "../../utils/aws.js";
3
+ export async function getSecretValue(secretId, region) {
4
+ return runAwsJsonAsync([
5
+ "secretsmanager",
6
+ "get-secret-value",
7
+ "--secret-id",
8
+ secretId,
9
+ ...buildRegionArgs(region),
10
+ ]);
11
+ }
12
+ export async function describeSecret(secretId, region) {
13
+ return runAwsJsonAsync([
14
+ "secretsmanager",
15
+ "describe-secret",
16
+ "--secret-id",
17
+ secretId,
18
+ ...buildRegionArgs(region),
19
+ ]);
20
+ }
21
+ export async function putSecretValue(secretId, value, region) {
22
+ await runAwsJsonAsync([
23
+ "secretsmanager",
24
+ "put-secret-value",
25
+ "--secret-id",
26
+ secretId,
27
+ "--secret-string",
28
+ value,
29
+ ...buildRegionArgs(region),
30
+ ]);
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a9s/cli",
3
- "version": "1.0.10",
3
+ "version": "1.1.0",
4
4
  "description": "k9s-style TUI navigator for AWS services",
5
5
  "keywords": [
6
6
  "aws",