@a9s/cli 1.0.7 → 1.0.9

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 (41) hide show
  1. package/dist/scripts/seed.js +202 -4
  2. package/dist/src/App.js +35 -3
  3. package/dist/src/components/AdvancedTextInput.js +3 -1
  4. package/dist/src/components/AutocompleteInput.js +3 -1
  5. package/dist/src/components/DetailPanel.js +3 -1
  6. package/dist/src/components/DiffViewer.js +3 -1
  7. package/dist/src/components/ErrorStatePanel.js +3 -1
  8. package/dist/src/components/HUD.js +3 -1
  9. package/dist/src/components/HelpPanel.js +6 -4
  10. package/dist/src/components/ModeBar.js +5 -8
  11. package/dist/src/components/Table/index.js +19 -26
  12. package/dist/src/components/TableSkeleton.js +3 -1
  13. package/dist/src/components/YankHelpPanel.js +3 -1
  14. package/dist/src/constants/commands.js +2 -1
  15. package/dist/src/constants/theme.js +608 -0
  16. package/dist/src/contexts/ThemeContext.js +13 -0
  17. package/dist/src/features/AppMainView.integration.test.js +1 -0
  18. package/dist/src/features/AppMainView.js +6 -4
  19. package/dist/src/hooks/useCommandRouter.js +5 -0
  20. package/dist/src/hooks/usePickerManager.js +35 -1
  21. package/dist/src/index.js +2 -1
  22. package/dist/src/services.js +2 -2
  23. package/dist/src/state/atoms.js +3 -0
  24. package/dist/src/utils/config.js +36 -0
  25. package/dist/src/views/dynamodb/adapter.js +313 -9
  26. package/dist/src/views/dynamodb/capabilities/detailCapability.js +94 -0
  27. package/dist/src/views/dynamodb/capabilities/yankCapability.js +6 -0
  28. package/dist/src/views/dynamodb/capabilities/yankOptions.js +69 -0
  29. package/dist/src/views/dynamodb/schema.js +18 -0
  30. package/dist/src/views/dynamodb/types.js +1 -0
  31. package/dist/src/views/dynamodb/utils.js +175 -0
  32. package/dist/src/views/iam/adapter.js +2 -1
  33. package/dist/src/views/route53/adapter.js +166 -9
  34. package/dist/src/views/route53/capabilities/detailCapability.js +63 -0
  35. package/dist/src/views/route53/capabilities/yankCapability.js +6 -0
  36. package/dist/src/views/route53/capabilities/yankOptions.js +58 -0
  37. package/dist/src/views/route53/schema.js +18 -0
  38. package/dist/src/views/route53/types.js +1 -0
  39. package/dist/src/views/s3/adapter.js +2 -1
  40. package/dist/src/views/secretsmanager/adapter.js +2 -1
  41. package/package.json +2 -1
@@ -0,0 +1,94 @@
1
+ import { runAwsJsonAsync } from "../../../utils/aws.js";
2
+ import { formatBillingMode, formatKeySchema } from "../utils.js";
3
+ export function createDynamoDBDetailCapability(region, getLevel) {
4
+ const regionArgs = region ? ["--region", region] : [];
5
+ const getDetails = async (row) => {
6
+ const meta = row.meta;
7
+ if (!meta) {
8
+ return [];
9
+ }
10
+ const level = getLevel?.();
11
+ // Table level
12
+ if (level?.kind === "tables" && meta.type === "table") {
13
+ try {
14
+ const data = await runAwsJsonAsync([
15
+ "dynamodb",
16
+ "describe-table",
17
+ "--table-name",
18
+ meta.tableName,
19
+ ...regionArgs,
20
+ ]);
21
+ const table = data.Table;
22
+ const fields = [
23
+ { label: "Table Name", value: table.TableName },
24
+ { label: "Table ARN", value: table.TableArn },
25
+ { label: "Status", value: table.TableStatus },
26
+ { label: "Billing Mode", value: formatBillingMode(table) },
27
+ { label: "Item Count", value: String(table.ItemCount ?? 0) },
28
+ { label: "Table Size", value: `${table.TableSizeBytes ?? 0} bytes` },
29
+ { label: "Key Schema", value: formatKeySchema(table) },
30
+ ];
31
+ // Add attributes
32
+ if (table.AttributeDefinitions && table.AttributeDefinitions.length > 0) {
33
+ const attrs = table.AttributeDefinitions.map((a) => `${a.AttributeName} (${a.AttributeType})`).join(", ");
34
+ fields.push({ label: "Attributes", value: attrs });
35
+ }
36
+ fields.push({
37
+ label: "GSI Count",
38
+ value: String(table.GlobalSecondaryIndexes?.length ?? 0),
39
+ });
40
+ fields.push({
41
+ label: "LSI Count",
42
+ value: String(table.LocalSecondaryIndexes?.length ?? 0),
43
+ });
44
+ if (table.CreationDateTime) {
45
+ fields.push({
46
+ label: "Created",
47
+ value: new Date(table.CreationDateTime).toISOString(),
48
+ });
49
+ }
50
+ return fields;
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ }
56
+ // Item level
57
+ if (level?.kind === "items" && meta.type === "item") {
58
+ try {
59
+ const itemJson = meta.itemJson ? JSON.parse(meta.itemJson) : {};
60
+ const prettyJson = JSON.stringify(itemJson, null, 2);
61
+ const fields = [
62
+ {
63
+ label: "Item (JSON)",
64
+ value: prettyJson,
65
+ },
66
+ ];
67
+ if (meta.itemSize) {
68
+ fields.push({
69
+ label: "Size",
70
+ value: `${meta.itemSize} bytes`,
71
+ });
72
+ }
73
+ return fields;
74
+ }
75
+ catch {
76
+ return [];
77
+ }
78
+ }
79
+ // Item-field level
80
+ if (level?.kind === "item-fields" && meta.type === "item-field") {
81
+ const fields = [
82
+ { label: "Attribute", value: meta.fieldName ?? "-" },
83
+ { label: "Type", value: meta.fieldType ?? "-" },
84
+ {
85
+ label: "Value",
86
+ value: String(meta.fieldRawValue ?? "-"),
87
+ },
88
+ ];
89
+ return fields;
90
+ }
91
+ return [];
92
+ };
93
+ return { getDetails };
94
+ }
@@ -0,0 +1,6 @@
1
+ import { createYankCapability } from "../../../adapters/capabilities/YankCapability.js";
2
+ import { DynamoDBRowMetaSchema } from "../schema.js";
3
+ import { DynamoDBYankOptions } from "./yankOptions.js";
4
+ export function createDynamoDBYankCapability(_region, _getLevel) {
5
+ return createYankCapability(DynamoDBYankOptions, DynamoDBRowMetaSchema, {});
6
+ }
@@ -0,0 +1,69 @@
1
+ export const DynamoDBYankOptions = [
2
+ // Table options
3
+ {
4
+ trigger: { type: "key", char: "n" },
5
+ label: "Copy table name",
6
+ feedback: "Copied table name",
7
+ isRelevant: (row) => row.meta.type === "table",
8
+ resolve: async (row) => {
9
+ return row.meta.tableName ?? null;
10
+ },
11
+ },
12
+ {
13
+ trigger: { type: "key", char: "a" },
14
+ label: "Copy table ARN",
15
+ feedback: "Copied table ARN",
16
+ isRelevant: (row) => row.meta.type === "table",
17
+ resolve: async (row) => {
18
+ return row.meta.tableArn ?? null;
19
+ },
20
+ },
21
+ // Item options
22
+ {
23
+ trigger: { type: "key", char: "k" },
24
+ label: "Copy primary key (JSON)",
25
+ feedback: "Copied primary key",
26
+ isRelevant: (row) => row.meta.type === "item",
27
+ resolve: async (row) => {
28
+ if (!row.meta.itemPkValue && !row.meta.itemSkValue) {
29
+ return null;
30
+ }
31
+ const obj = {};
32
+ if (row.meta.itemPkValue) {
33
+ obj.pk = row.meta.itemPkValue;
34
+ }
35
+ if (row.meta.itemSkValue) {
36
+ obj.sk = row.meta.itemSkValue;
37
+ }
38
+ return JSON.stringify(obj);
39
+ },
40
+ },
41
+ {
42
+ trigger: { type: "key", char: "j" },
43
+ label: "Copy entire item (JSON)",
44
+ feedback: "Copied item",
45
+ isRelevant: (row) => row.meta.type === "item" && !!row.meta.itemJson,
46
+ resolve: async (row) => {
47
+ return row.meta.itemJson ?? null;
48
+ },
49
+ },
50
+ // Field options
51
+ {
52
+ trigger: { type: "key", char: "v" },
53
+ label: "Copy attribute value",
54
+ feedback: "Copied attribute value",
55
+ isRelevant: (row) => row.meta.type === "item-field",
56
+ resolve: async (row) => {
57
+ return row.meta.fieldValue ?? null;
58
+ },
59
+ },
60
+ {
61
+ trigger: { type: "key", char: "k" },
62
+ label: "Copy attribute name",
63
+ feedback: "Copied attribute name",
64
+ isRelevant: (row) => row.meta.type === "item-field",
65
+ resolve: async (row) => {
66
+ return row.meta.fieldName ?? null;
67
+ },
68
+ },
69
+ ];
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ export const DynamoDBRowMetaSchema = z.object({
3
+ type: z.enum(["table", "item", "item-field"]),
4
+ tableName: z.string().optional(),
5
+ tableStatus: z.string().optional(),
6
+ tableArn: z.string().optional(),
7
+ billing: z.string().optional(),
8
+ gsiCount: z.number().optional(),
9
+ itemIndex: z.number().optional(),
10
+ itemPkValue: z.string().optional(),
11
+ itemSkValue: z.string().optional(),
12
+ itemSize: z.number().optional(),
13
+ itemJson: z.string().optional(),
14
+ fieldName: z.string().optional(),
15
+ fieldValue: z.string().optional(),
16
+ fieldType: z.string().optional(),
17
+ fieldRawValue: z.unknown().optional(),
18
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Unwrap a DynamoDB AttributeValue to its native JS value.
3
+ * E.g., { S: "hello" } → "hello", { N: "42" } → "42", { BOOL: true } → true
4
+ */
5
+ export function unwrapDynamoValue(attr) {
6
+ const key = Object.keys(attr)[0];
7
+ const value = attr[key];
8
+ switch (key) {
9
+ case "S":
10
+ return value;
11
+ case "N":
12
+ return value;
13
+ case "B":
14
+ return value;
15
+ case "SS":
16
+ return value;
17
+ case "NS":
18
+ return value;
19
+ case "BS":
20
+ return value;
21
+ case "M": {
22
+ const mapVal = value;
23
+ const obj = {};
24
+ for (const [k, v] of Object.entries(mapVal)) {
25
+ obj[k] = unwrapDynamoValue(v);
26
+ }
27
+ return obj;
28
+ }
29
+ case "L": {
30
+ const listVal = value;
31
+ return listVal.map((v) => unwrapDynamoValue(v));
32
+ }
33
+ case "NULL":
34
+ return null;
35
+ case "BOOL":
36
+ return value;
37
+ default:
38
+ return undefined;
39
+ }
40
+ }
41
+ /**
42
+ * Get the DynamoDB type descriptor for an attribute value.
43
+ * Returns the single-letter type: S, N, B, SS, NS, BS, M, L, NULL, BOOL
44
+ */
45
+ export function getDynamoType(attr) {
46
+ const key = Object.keys(attr)[0];
47
+ return key || "?";
48
+ }
49
+ /**
50
+ * Format a DynamoDB value for display.
51
+ * - For simple types (S, N, B, BOOL, NULL), return the value
52
+ * - For collections (SS, NS, BS), return comma-separated or wrapped in []
53
+ * - For maps/lists, return pretty JSON
54
+ * - For large values (>3 lines), truncate to 2 header + ... + 1 footer
55
+ * - For single/few line values, truncate at 50 chars with "..." suffix
56
+ */
57
+ export function formatDynamoValue(attr) {
58
+ const type = getDynamoType(attr);
59
+ const raw = unwrapDynamoValue(attr);
60
+ const key = Object.keys(attr)[0];
61
+ const value = attr[key];
62
+ if (type === "NULL")
63
+ return "null";
64
+ if (type === "BOOL")
65
+ return String(value);
66
+ if (type === "N")
67
+ return String(value);
68
+ if (type === "S") {
69
+ const str = String(value);
70
+ // Escape newlines for display
71
+ const escaped = str.replace(/\n/g, "\\n");
72
+ if (escaped.length > 50)
73
+ return escaped.slice(0, 47) + "...";
74
+ return escaped;
75
+ }
76
+ if (type === "B")
77
+ return `<binary: ${value?.length ?? 0} bytes>`;
78
+ if (type === "SS") {
79
+ return (value ?? []).join(", ");
80
+ }
81
+ if (type === "NS") {
82
+ return (value ?? []).join(", ");
83
+ }
84
+ if (type === "BS") {
85
+ return `<${(value ?? []).length} binary values>`;
86
+ }
87
+ if (type === "M") {
88
+ const json = JSON.stringify(raw, null, 2);
89
+ return truncateLargeValue(json);
90
+ }
91
+ if (type === "L") {
92
+ const json = JSON.stringify(raw, null, 2);
93
+ return truncateLargeValue(json);
94
+ }
95
+ return String(raw);
96
+ }
97
+ function truncateLargeValue(str) {
98
+ const lines = str.split("\n");
99
+ if (lines.length > 3) {
100
+ return `${lines[0]}\n${lines[1]}\n...\n${lines[lines.length - 1]}`;
101
+ }
102
+ // Single or few lines
103
+ const compact = str.replace(/\n/g, " ");
104
+ if (compact.length > 50)
105
+ return compact.slice(0, 47) + "...";
106
+ return compact;
107
+ }
108
+ /**
109
+ * Format billing mode: "On-Demand" or "Provisioned (R:X W:Y)"
110
+ */
111
+ export function formatBillingMode(table) {
112
+ const billing = table.BillingModeSummary?.BillingMode;
113
+ if (billing === "PAY_PER_REQUEST") {
114
+ return "On-Demand";
115
+ }
116
+ const r = table.ProvisionedThroughput?.ReadCapacityUnits ?? 0;
117
+ const w = table.ProvisionedThroughput?.WriteCapacityUnits ?? 0;
118
+ return `Provisioned (R:${r} W:${w})`;
119
+ }
120
+ /**
121
+ * Format key schema: "PK: userId, SK: timestamp" or just "PK: id"
122
+ */
123
+ export function formatKeySchema(table) {
124
+ const keys = table.KeySchema ?? [];
125
+ const hash = keys.find((k) => k.KeyType === "HASH");
126
+ const range = keys.find((k) => k.KeyType === "RANGE");
127
+ let result = "";
128
+ if (hash)
129
+ result += `PK: ${hash.AttributeName}`;
130
+ if (range)
131
+ result += (result ? ", " : "") + `SK: ${range.AttributeName}`;
132
+ return result || "-";
133
+ }
134
+ /**
135
+ * Extract primary key value from an item.
136
+ * Returns a JSON string like '{"userId": "abc123"}' or '{"id": "x", "sk": "y"}'
137
+ */
138
+ export function extractPrimaryKeyJson(item, table) {
139
+ const keys = table.KeySchema ?? [];
140
+ const pkObj = {};
141
+ for (const key of keys) {
142
+ const attrName = key.AttributeName;
143
+ const attr = item[attrName];
144
+ if (attr) {
145
+ pkObj[attrName] = unwrapDynamoValue(attr);
146
+ }
147
+ }
148
+ return JSON.stringify(pkObj);
149
+ }
150
+ /**
151
+ * Get the sort key value from an item for display.
152
+ * Returns unwrapped value or undefined.
153
+ */
154
+ export function extractSkValue(item, table) {
155
+ const rangeKey = table.KeySchema?.find((k) => k.KeyType === "RANGE");
156
+ if (!rangeKey)
157
+ return undefined;
158
+ const attr = item[rangeKey.AttributeName];
159
+ if (!attr)
160
+ return undefined;
161
+ return String(unwrapDynamoValue(attr));
162
+ }
163
+ /**
164
+ * Get the partition key value from an item for display.
165
+ * Returns unwrapped value or undefined.
166
+ */
167
+ export function extractPkValue(item, table) {
168
+ const hashKey = table.KeySchema?.find((k) => k.KeyType === "HASH");
169
+ if (!hashKey)
170
+ return undefined;
171
+ const attr = item[hashKey.AttributeName];
172
+ if (!attr)
173
+ return undefined;
174
+ return String(unwrapDynamoValue(attr));
175
+ }
@@ -4,6 +4,7 @@ import { formatDate } from "./utils.js";
4
4
  import { createIamEditCapability } from "./capabilities/editCapability.js";
5
5
  import { createIamDetailCapability } from "./capabilities/detailCapability.js";
6
6
  import { createIamYankCapability } from "./capabilities/yankCapability.js";
7
+ import { SERVICE_COLORS } from "../../constants/theme.js";
7
8
  function getIamMeta(row) {
8
9
  return row.meta;
9
10
  }
@@ -241,7 +242,7 @@ export function createIamServiceAdapter() {
241
242
  return {
242
243
  id: "iam",
243
244
  label: "IAM",
244
- hudColor: { bg: "magenta", fg: "white" },
245
+ hudColor: SERVICE_COLORS.iam,
245
246
  getColumns,
246
247
  getRows,
247
248
  onSelect,
@@ -1,22 +1,179 @@
1
1
  import { textCell } from "../../types.js";
2
- export function createRoute53ServiceAdapter() {
3
- const getColumns = () => [{ key: "name", label: "Name" }];
2
+ import { runAwsJsonAsync } from "../../utils/aws.js";
3
+ import { atom } from "jotai";
4
+ import { getDefaultStore } from "jotai";
5
+ import { createRoute53DetailCapability } from "./capabilities/detailCapability.js";
6
+ import { createRoute53YankCapability } from "./capabilities/yankCapability.js";
7
+ import { SERVICE_COLORS } from "../../constants/theme.js";
8
+ export const route53LevelAtom = atom({ kind: "zones" });
9
+ export const route53BackStackAtom = atom([]);
10
+ export function createRoute53ServiceAdapter(endpointUrl, region) {
11
+ const store = getDefaultStore();
12
+ const regionArgs = region ? ["--region", region] : [];
13
+ const getLevel = () => store.get(route53LevelAtom);
14
+ const setLevel = (level) => store.set(route53LevelAtom, level);
15
+ const getBackStack = () => store.get(route53BackStackAtom);
16
+ const setBackStack = (stack) => store.set(route53BackStackAtom, stack);
17
+ const getColumns = () => {
18
+ const level = getLevel();
19
+ if (level.kind === "zones") {
20
+ return [
21
+ { key: "name", label: "Name" },
22
+ { key: "zoneId", label: "Zone ID" },
23
+ { key: "recordCount", label: "Records" },
24
+ { key: "type", label: "Type", width: 10 },
25
+ ];
26
+ }
27
+ // records level
28
+ return [
29
+ { key: "name", label: "Name" },
30
+ { key: "type", label: "Type", width: 10 },
31
+ { key: "ttl", label: "TTL", width: 10 },
32
+ { key: "values", label: "Value(s)", width: 50 },
33
+ ];
34
+ };
4
35
  const getRows = async () => {
5
- return [{ id: "stub", cells: { name: textCell("Route53 not yet implemented") }, meta: {} }];
36
+ const level = getLevel();
37
+ if (level.kind === "zones") {
38
+ const data = await runAwsJsonAsync([
39
+ "route53",
40
+ "list-hosted-zones",
41
+ ...regionArgs,
42
+ ]);
43
+ return (data.HostedZones ?? []).map((zone) => {
44
+ const isPrivate = zone.Config?.PrivateZone ?? zone.HostedZoneConfig?.PrivateZone ?? false;
45
+ const shortZoneId = zone.Id.replace(/^\/hostedzone\//, "");
46
+ return {
47
+ id: zone.Id,
48
+ cells: {
49
+ name: textCell(zone.Name),
50
+ zoneId: textCell(shortZoneId),
51
+ recordCount: textCell(String(zone.ResourceRecordSetCount ?? 0)),
52
+ type: textCell(isPrivate ? "Private" : "Public"),
53
+ },
54
+ meta: {
55
+ type: "zone",
56
+ zoneId: shortZoneId,
57
+ zoneName: zone.Name,
58
+ isPrivate,
59
+ },
60
+ };
61
+ });
62
+ }
63
+ // records level
64
+ const { zoneId, zoneName } = level;
65
+ try {
66
+ const fullZoneId = zoneId.startsWith("/hostedzone/") ? zoneId : `/hostedzone/${zoneId}`;
67
+ const data = await runAwsJsonAsync([
68
+ "route53",
69
+ "list-resource-record-sets",
70
+ "--hosted-zone-id",
71
+ fullZoneId,
72
+ ...regionArgs,
73
+ ]);
74
+ return (data.ResourceRecordSets ?? []).map((record) => {
75
+ const values = [];
76
+ let valuesDisplay = "";
77
+ if (record.AliasTarget) {
78
+ valuesDisplay = `ALIAS → ${record.AliasTarget.DNSName}`;
79
+ }
80
+ else if (record.ResourceRecords && record.ResourceRecords.length > 0) {
81
+ record.ResourceRecords.forEach((r) => values.push(r.Value));
82
+ if (values.length <= 3) {
83
+ valuesDisplay = values.join(", ");
84
+ }
85
+ else {
86
+ valuesDisplay = `<${values.length} values>`;
87
+ }
88
+ }
89
+ return {
90
+ id: `${record.Name}-${record.Type}`,
91
+ cells: {
92
+ name: textCell(record.Name),
93
+ type: textCell(record.Type),
94
+ ttl: textCell(record.AliasTarget ? "-" : String(record.TTL ?? "-")),
95
+ values: textCell(valuesDisplay),
96
+ },
97
+ meta: {
98
+ type: "record",
99
+ zoneId,
100
+ zoneName,
101
+ recordName: record.Name,
102
+ recordType: record.Type,
103
+ recordTtl: record.TTL,
104
+ recordValues: values,
105
+ recordAliasTarget: record.AliasTarget,
106
+ },
107
+ };
108
+ });
109
+ }
110
+ catch {
111
+ return [];
112
+ }
6
113
  };
7
- const onSelect = async (_row) => {
114
+ const onSelect = async (row) => {
115
+ const level = getLevel();
116
+ const backStack = getBackStack();
117
+ const meta = row.meta;
118
+ if (level.kind === "zones") {
119
+ if (!meta || meta.type !== "zone") {
120
+ return { action: "none" };
121
+ }
122
+ const newStack = [...backStack, { level: level, selectedIndex: 0 }];
123
+ setBackStack(newStack);
124
+ setLevel({
125
+ kind: "records",
126
+ zoneId: meta.zoneId,
127
+ zoneName: meta.zoneName,
128
+ });
129
+ return { action: "navigate" };
130
+ }
131
+ // records level: leaf, no drill-down
8
132
  return { action: "none" };
9
133
  };
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
+ };
144
+ const getPath = () => {
145
+ const level = getLevel();
146
+ if (level.kind === "zones")
147
+ return "route53://";
148
+ return `route53://${level.zoneName}`;
149
+ };
150
+ const getContextLabel = () => {
151
+ const level = getLevel();
152
+ if (level.kind === "zones")
153
+ return "🌐 Hosted Zones";
154
+ return `🌐 ${level.zoneName}`;
155
+ };
156
+ // Compose capabilities
157
+ const detailCapability = createRoute53DetailCapability(region, getLevel);
158
+ const yankCapability = createRoute53YankCapability(region, getLevel);
10
159
  return {
11
160
  id: "route53",
12
161
  label: "Route53",
13
- hudColor: { bg: "blue", fg: "white" },
162
+ hudColor: SERVICE_COLORS.route53,
14
163
  getColumns,
15
164
  getRows,
16
165
  onSelect,
17
- canGoBack: () => false,
18
- goBack: () => { },
19
- getPath: () => "/",
20
- getContextLabel: () => "🌐 DNS Records",
166
+ canGoBack,
167
+ goBack,
168
+ getPath,
169
+ getContextLabel,
170
+ reset() {
171
+ setLevel({ kind: "zones" });
172
+ setBackStack([]);
173
+ },
174
+ capabilities: {
175
+ detail: detailCapability,
176
+ yank: yankCapability,
177
+ },
21
178
  };
22
179
  }
@@ -0,0 +1,63 @@
1
+ import { runAwsJsonAsync } from "../../../utils/aws.js";
2
+ export function createRoute53DetailCapability(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
+ // Zone level
11
+ if (level?.kind === "zones" && meta.type === "zone") {
12
+ const fullZoneId = meta.zoneId.startsWith("/hostedzone/")
13
+ ? meta.zoneId
14
+ : `/hostedzone/${meta.zoneId}`;
15
+ const data = await runAwsJsonAsync([
16
+ "route53",
17
+ "get-hosted-zone",
18
+ "--id",
19
+ fullZoneId,
20
+ ...regionArgs,
21
+ ]);
22
+ const fields = [
23
+ { label: "Zone Name", value: data.Name ?? "-" },
24
+ { label: "Zone ID (Full)", value: data.Id ?? "-" },
25
+ { label: "Zone ID (Short)", value: meta.zoneId },
26
+ { label: "Record Count", value: String(data.ResourceRecordSetCount ?? 0) },
27
+ {
28
+ label: "Type",
29
+ value: meta.isPrivate ? "Private" : "Public",
30
+ },
31
+ { label: "Caller Reference", value: data.CallerReference ?? "-" },
32
+ ];
33
+ return fields;
34
+ }
35
+ // Record level
36
+ if (level?.kind === "records" && meta.type === "record") {
37
+ const fields = [
38
+ { label: "Record Name", value: meta.recordName ?? "-" },
39
+ { label: "Record Type", value: meta.recordType ?? "-" },
40
+ { label: "TTL", value: meta.recordAliasTarget ? "(Alias - no TTL)" : String(meta.recordTtl ?? "-") },
41
+ ];
42
+ if (meta.recordValues && meta.recordValues.length > 0) {
43
+ fields.push({
44
+ label: "Values",
45
+ value: meta.recordValues.join("\n"),
46
+ });
47
+ }
48
+ if (meta.recordAliasTarget) {
49
+ fields.push({
50
+ label: "Alias Target",
51
+ value: meta.recordAliasTarget.DNSName,
52
+ });
53
+ fields.push({
54
+ label: "Hosted Zone ID",
55
+ value: meta.recordAliasTarget.HostedZoneId,
56
+ });
57
+ }
58
+ return fields;
59
+ }
60
+ return [];
61
+ };
62
+ return { getDetails };
63
+ }
@@ -0,0 +1,6 @@
1
+ import { createYankCapability } from "../../../adapters/capabilities/YankCapability.js";
2
+ import { Route53RowMetaSchema } from "../schema.js";
3
+ import { Route53YankOptions } from "./yankOptions.js";
4
+ export function createRoute53YankCapability(_region, _getLevel) {
5
+ return createYankCapability(Route53YankOptions, Route53RowMetaSchema, {});
6
+ }