@a9s/cli 0.0.1 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +167 -2
  2. package/dist/scripts/seed.js +310 -0
  3. package/dist/src/App.js +476 -0
  4. package/dist/src/adapters/ServiceAdapter.js +1 -0
  5. package/dist/src/adapters/capabilities/ActionCapability.js +1 -0
  6. package/dist/src/adapters/capabilities/DetailCapability.js +1 -0
  7. package/dist/src/adapters/capabilities/EditCapability.js +1 -0
  8. package/dist/src/adapters/capabilities/YankCapability.js +42 -0
  9. package/dist/src/adapters/capabilities/YankCapability.test.js +29 -0
  10. package/dist/src/components/AdvancedTextInput.js +200 -0
  11. package/dist/src/components/AdvancedTextInput.test.js +190 -0
  12. package/dist/src/components/AutocompleteInput.js +29 -0
  13. package/dist/src/components/DetailPanel.js +12 -0
  14. package/dist/src/components/DiffViewer.js +17 -0
  15. package/dist/src/components/ErrorStatePanel.js +5 -0
  16. package/dist/src/components/HUD.js +31 -0
  17. package/dist/src/components/HelpPanel.js +33 -0
  18. package/dist/src/components/ModeBar.js +43 -0
  19. package/dist/src/components/Table/index.js +109 -0
  20. package/dist/src/components/Table/widths.js +19 -0
  21. package/dist/src/components/TableSkeleton.js +25 -0
  22. package/dist/src/components/YankHelpPanel.js +43 -0
  23. package/dist/src/constants/commands.js +15 -0
  24. package/dist/src/constants/keybindings.js +530 -0
  25. package/dist/src/constants/keys.js +37 -0
  26. package/dist/src/features/AppMainView.integration.test.js +133 -0
  27. package/dist/src/features/AppMainView.js +95 -0
  28. package/dist/src/hooks/inputEvents.js +1 -0
  29. package/dist/src/hooks/mainInputScopes.js +68 -0
  30. package/dist/src/hooks/mainInputScopes.test.js +24 -0
  31. package/dist/src/hooks/useActionController.js +78 -0
  32. package/dist/src/hooks/useAppController.js +102 -0
  33. package/dist/src/hooks/useAppController.test.js +54 -0
  34. package/dist/src/hooks/useAppData.js +48 -0
  35. package/dist/src/hooks/useAwsContext.js +77 -0
  36. package/dist/src/hooks/useAwsProfiles.js +53 -0
  37. package/dist/src/hooks/useAwsRegions.js +105 -0
  38. package/dist/src/hooks/useCommandRouter.js +56 -0
  39. package/dist/src/hooks/useCommandRouter.test.js +27 -0
  40. package/dist/src/hooks/useDetailController.js +57 -0
  41. package/dist/src/hooks/useDetailController.test.js +32 -0
  42. package/dist/src/hooks/useHelpPanel.js +65 -0
  43. package/dist/src/hooks/useHierarchyState.js +39 -0
  44. package/dist/src/hooks/useInputEventProcessor.js +450 -0
  45. package/dist/src/hooks/useInputEventProcessor.test.js +174 -0
  46. package/dist/src/hooks/useKeyChord.js +83 -0
  47. package/dist/src/hooks/useMainInput.js +18 -0
  48. package/dist/src/hooks/useNavigation.js +47 -0
  49. package/dist/src/hooks/usePendingAction.js +8 -0
  50. package/dist/src/hooks/usePickerManager.js +130 -0
  51. package/dist/src/hooks/usePickerState.js +47 -0
  52. package/dist/src/hooks/usePickerTable.js +20 -0
  53. package/dist/src/hooks/useServiceView.js +226 -0
  54. package/dist/src/hooks/useUiHints.js +60 -0
  55. package/dist/src/hooks/useYankMode.js +24 -0
  56. package/dist/src/hooks/yankHeaderMarkers.js +23 -0
  57. package/dist/src/hooks/yankHeaderMarkers.test.js +49 -0
  58. package/dist/src/index.js +30 -0
  59. package/dist/src/services.js +12 -0
  60. package/dist/src/state/atoms.js +27 -0
  61. package/dist/src/types.js +12 -0
  62. package/dist/src/utils/aws.js +39 -0
  63. package/dist/src/utils/debugLogger.js +34 -0
  64. package/dist/src/utils/secretDisplay.js +45 -0
  65. package/dist/src/utils/withFullscreen.js +38 -0
  66. package/dist/src/views/dynamodb/adapter.js +22 -0
  67. package/dist/src/views/iam/adapter.js +258 -0
  68. package/dist/src/views/iam/capabilities/detailCapability.js +93 -0
  69. package/dist/src/views/iam/capabilities/editCapability.js +59 -0
  70. package/dist/src/views/iam/capabilities/yankCapability.js +6 -0
  71. package/dist/src/views/iam/capabilities/yankOptions.js +15 -0
  72. package/dist/src/views/iam/schema.js +7 -0
  73. package/dist/src/views/iam/types.js +1 -0
  74. package/dist/src/views/iam/utils.js +21 -0
  75. package/dist/src/views/route53/adapter.js +22 -0
  76. package/dist/src/views/s3/adapter.js +154 -0
  77. package/dist/src/views/s3/capabilities/actionCapability.js +172 -0
  78. package/dist/src/views/s3/capabilities/detailCapability.js +115 -0
  79. package/dist/src/views/s3/capabilities/editCapability.js +35 -0
  80. package/dist/src/views/s3/capabilities/yankCapability.js +6 -0
  81. package/dist/src/views/s3/capabilities/yankOptions.js +55 -0
  82. package/dist/src/views/s3/client.js +12 -0
  83. package/dist/src/views/s3/fetcher.js +86 -0
  84. package/dist/src/views/s3/schema.js +6 -0
  85. package/dist/src/views/s3/utils.js +19 -0
  86. package/dist/src/views/secretsmanager/adapter.js +188 -0
  87. package/dist/src/views/secretsmanager/capabilities/actionCapability.js +193 -0
  88. package/dist/src/views/secretsmanager/capabilities/detailCapability.js +46 -0
  89. package/dist/src/views/secretsmanager/capabilities/editCapability.js +116 -0
  90. package/dist/src/views/secretsmanager/capabilities/yankCapability.js +7 -0
  91. package/dist/src/views/secretsmanager/capabilities/yankOptions.js +68 -0
  92. package/dist/src/views/secretsmanager/schema.js +28 -0
  93. package/dist/src/views/secretsmanager/types.js +1 -0
  94. package/package.json +72 -5
  95. package/index.js +0 -1
@@ -0,0 +1,105 @@
1
+ import { execFile } from "child_process";
2
+ import { promisify } from "util";
3
+ import { useEffect, useState } from "react";
4
+ const execFileAsync = promisify(execFile);
5
+ const STATIC_FALLBACK_REGIONS = [
6
+ "us-east-1",
7
+ "us-east-2",
8
+ "us-west-1",
9
+ "us-west-2",
10
+ "ca-central-1",
11
+ "ca-west-1",
12
+ "sa-east-1",
13
+ "eu-west-1",
14
+ "eu-west-2",
15
+ "eu-west-3",
16
+ "eu-central-1",
17
+ "eu-central-2",
18
+ "eu-north-1",
19
+ "eu-south-1",
20
+ "eu-south-2",
21
+ "af-south-1",
22
+ "ap-south-1",
23
+ "ap-south-2",
24
+ "ap-southeast-1",
25
+ "ap-southeast-2",
26
+ "ap-southeast-3",
27
+ "ap-southeast-4",
28
+ "ap-northeast-1",
29
+ "ap-northeast-2",
30
+ "ap-northeast-3",
31
+ "me-south-1",
32
+ "me-central-1",
33
+ "il-central-1",
34
+ ];
35
+ let cachedRegions = null;
36
+ let pendingRegionsPromise = null;
37
+ async function fetchRegions(profile, region) {
38
+ const args = [
39
+ "ec2",
40
+ "describe-regions",
41
+ "--all-regions",
42
+ "--query",
43
+ "Regions[].RegionName",
44
+ "--output",
45
+ "json",
46
+ ];
47
+ const env = { ...process.env };
48
+ if (profile)
49
+ env.AWS_PROFILE = profile;
50
+ if (region)
51
+ env.AWS_REGION = region;
52
+ try {
53
+ const { stdout } = await execFileAsync("aws", args, {
54
+ timeout: 3000,
55
+ env,
56
+ });
57
+ const parsed = JSON.parse(stdout);
58
+ if (Array.isArray(parsed)) {
59
+ const normalized = parsed
60
+ .filter((x) => typeof x === "string")
61
+ .map((x) => x.trim())
62
+ .filter(Boolean)
63
+ .sort((a, b) => a.localeCompare(b));
64
+ if (normalized.length > 0)
65
+ return normalized;
66
+ }
67
+ }
68
+ catch {
69
+ // fall back below
70
+ }
71
+ return [...STATIC_FALLBACK_REGIONS];
72
+ }
73
+ export function useAwsRegions(selectedRegion, selectedProfile) {
74
+ const isLocal = Boolean(process.env.AWS_ENDPOINT_URL);
75
+ const toOptions = (regions) => regions.map((name) => ({
76
+ name,
77
+ description: isLocal ? "Local endpoint / emulated" : "AWS commercial region",
78
+ }));
79
+ const [regionOptions, setRegionOptions] = useState(toOptions(cachedRegions ?? [...STATIC_FALLBACK_REGIONS]));
80
+ const explicitProfile = selectedProfile && selectedProfile !== "$default" ? selectedProfile : undefined;
81
+ useEffect(() => {
82
+ let alive = true;
83
+ if (cachedRegions) {
84
+ setRegionOptions(toOptions(cachedRegions));
85
+ return () => {
86
+ alive = false;
87
+ };
88
+ }
89
+ if (!pendingRegionsPromise) {
90
+ pendingRegionsPromise = fetchRegions(explicitProfile ?? process.env.AWS_PROFILE, selectedRegion ?? process.env.AWS_REGION).then((result) => {
91
+ cachedRegions = result;
92
+ return result;
93
+ });
94
+ }
95
+ void pendingRegionsPromise.then((result) => {
96
+ if (!alive)
97
+ return;
98
+ setRegionOptions(toOptions(result));
99
+ });
100
+ return () => {
101
+ alive = false;
102
+ };
103
+ }, [explicitProfile, selectedRegion]);
104
+ return regionOptions;
105
+ }
@@ -0,0 +1,56 @@
1
+ import { useCallback } from "react";
2
+ import { SERVICE_REGISTRY } from "../services.js";
3
+ export function parseCommand(input) {
4
+ const command = input.trim();
5
+ if (command === "profiles")
6
+ return { type: "openProfiles" };
7
+ if (command === "regions")
8
+ return { type: "openRegions" };
9
+ if (command === "resources")
10
+ return { type: "openResources" };
11
+ const regionMatch = command.match(/^(region|use-region)\s+([a-z0-9-]+)$/i);
12
+ if (regionMatch?.[2]) {
13
+ return { type: "setRegion", region: regionMatch[2].toLowerCase() };
14
+ }
15
+ const profileMatch = command.match(/^(profile|use-profile)\s+(.+)$/i);
16
+ if (profileMatch?.[2]) {
17
+ return { type: "setProfile", profile: profileMatch[2].trim() };
18
+ }
19
+ if (command === "quit" || command === "q") {
20
+ return { type: "quit" };
21
+ }
22
+ if (command in SERVICE_REGISTRY) {
23
+ return { type: "switchService", serviceId: command };
24
+ }
25
+ return { type: "unknown" };
26
+ }
27
+ export function useCommandRouter(args) {
28
+ return useCallback((input) => {
29
+ const parsed = parseCommand(input);
30
+ switch (parsed.type) {
31
+ case "openProfiles":
32
+ args.openProfilePicker();
33
+ return;
34
+ case "openRegions":
35
+ args.openRegionPicker();
36
+ return;
37
+ case "openResources":
38
+ args.openResourcePicker();
39
+ return;
40
+ case "setRegion":
41
+ args.setSelectedRegion(parsed.region);
42
+ return;
43
+ case "setProfile":
44
+ args.setSelectedProfile(parsed.profile);
45
+ return;
46
+ case "quit":
47
+ args.exit();
48
+ return;
49
+ case "switchService":
50
+ args.switchAdapter(parsed.serviceId);
51
+ return;
52
+ case "unknown":
53
+ return;
54
+ }
55
+ }, [args]);
56
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseCommand } from "./useCommandRouter.js";
3
+ describe("parseCommand", () => {
4
+ it("parses picker commands", () => {
5
+ expect(parseCommand("profiles")).toEqual({ type: "openProfiles" });
6
+ expect(parseCommand("regions")).toEqual({ type: "openRegions" });
7
+ expect(parseCommand("resources")).toEqual({ type: "openResources" });
8
+ });
9
+ it("parses region and profile setters", () => {
10
+ expect(parseCommand("use-region EU-WEST-1")).toEqual({
11
+ type: "setRegion",
12
+ region: "eu-west-1",
13
+ });
14
+ expect(parseCommand("profile dev-admin ")).toEqual({
15
+ type: "setProfile",
16
+ profile: "dev-admin",
17
+ });
18
+ });
19
+ it("parses service switches and quit", () => {
20
+ expect(parseCommand("s3")).toEqual({ type: "switchService", serviceId: "s3" });
21
+ expect(parseCommand("q")).toEqual({ type: "quit" });
22
+ expect(parseCommand("quit")).toEqual({ type: "quit" });
23
+ });
24
+ it("returns unknown for unsupported commands", () => {
25
+ expect(parseCommand("something-else")).toEqual({ type: "unknown" });
26
+ });
27
+ });
@@ -0,0 +1,57 @@
1
+ import { useCallback, useRef } from "react";
2
+ import { getCellValue } from "../types.js";
3
+ export function applyDetailSuccess(prev, requestId, fields) {
4
+ if (!prev || prev.requestId !== requestId)
5
+ return prev;
6
+ return { ...prev, fields, loading: false };
7
+ }
8
+ export function applyDetailError(prev, requestId, selectedRow, error) {
9
+ if (!prev || prev.requestId !== requestId)
10
+ return prev;
11
+ return {
12
+ ...prev,
13
+ fields: [
14
+ {
15
+ label: "Name",
16
+ value: selectedRow.cells.name ? getCellValue(selectedRow.cells.name) : selectedRow.id,
17
+ },
18
+ { label: "Error", value: error.message },
19
+ ],
20
+ loading: false,
21
+ };
22
+ }
23
+ export function useDetailController({ adapter, setDescribeState }) {
24
+ const requestSeqRef = useRef(0);
25
+ const showDetails = useCallback((selectedRow) => {
26
+ if (!selectedRow)
27
+ return;
28
+ requestSeqRef.current += 1;
29
+ const requestId = requestSeqRef.current;
30
+ setDescribeState({ row: selectedRow, fields: null, loading: true, requestId });
31
+ void (async () => {
32
+ try {
33
+ const fields = adapter.capabilities?.detail
34
+ ? await adapter.capabilities.detail.getDetails(selectedRow)
35
+ : [
36
+ {
37
+ label: "Name",
38
+ value: selectedRow.cells.name
39
+ ? getCellValue(selectedRow.cells.name)
40
+ : selectedRow.id,
41
+ },
42
+ { label: "Type", value: String(selectedRow.meta?.type ?? "Unknown") },
43
+ { label: "Details", value: "Not available for this service" },
44
+ ];
45
+ setDescribeState((prev) => applyDetailSuccess(prev, requestId, fields));
46
+ }
47
+ catch (error) {
48
+ setDescribeState((prev) => applyDetailError(prev, requestId, selectedRow, error));
49
+ }
50
+ })();
51
+ }, [adapter, setDescribeState]);
52
+ const closeDetails = useCallback(() => setDescribeState(null), [setDescribeState]);
53
+ return {
54
+ showDetails,
55
+ closeDetails,
56
+ };
57
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { applyDetailError, applyDetailSuccess } from "./useDetailController.js";
3
+ import { textCell } from "../types.js";
4
+ describe("detail state stale-request guards", () => {
5
+ const row = { id: "file.txt", cells: { name: textCell("file.txt") } };
6
+ it("applies success only when request id is current", () => {
7
+ const current = {
8
+ row,
9
+ fields: null,
10
+ loading: true,
11
+ requestId: 4,
12
+ };
13
+ const staleResult = applyDetailSuccess(current, 3, [{ label: "Size", value: "10" }]);
14
+ expect(staleResult).toEqual(current);
15
+ const freshResult = applyDetailSuccess(current, 4, [{ label: "Size", value: "10" }]);
16
+ expect(freshResult?.loading).toBe(false);
17
+ expect(freshResult?.fields).toEqual([{ label: "Size", value: "10" }]);
18
+ });
19
+ it("applies error only when request id is current", () => {
20
+ const current = {
21
+ row,
22
+ fields: null,
23
+ loading: true,
24
+ requestId: 9,
25
+ };
26
+ const staleResult = applyDetailError(current, 8, row, new Error("boom"));
27
+ expect(staleResult).toEqual(current);
28
+ const freshResult = applyDetailError(current, 9, row, new Error("boom"));
29
+ expect(freshResult?.loading).toBe(false);
30
+ expect(freshResult?.fields?.[1]?.value).toBe("boom");
31
+ });
32
+ });
@@ -0,0 +1,65 @@
1
+ import { useState, useCallback } from "react";
2
+ export function useHelpPanel(helpTabs, helpContainerHeight) {
3
+ const [helpOpen, setHelpOpen] = useState(false);
4
+ const [helpTabIndex, setHelpTabIndex] = useState(0);
5
+ const [helpScrollOffset, setHelpScrollOffset] = useState(0);
6
+ const helpTabsCount = helpTabs.length;
7
+ // Compute visible rows from container height and current tab
8
+ const baseHelpVisibleRows = Math.max(1, helpContainerHeight - 3);
9
+ const activeHelpItemsCount = helpTabs[helpTabIndex]?.items.length ?? 0;
10
+ const overflowRows = Math.max(0, activeHelpItemsCount - baseHelpVisibleRows);
11
+ const scrollReserveRows = Math.min(3, overflowRows);
12
+ const helpVisibleRows = overflowRows > 0
13
+ ? Math.max(1, baseHelpVisibleRows - scrollReserveRows - 1)
14
+ : Math.max(1, baseHelpVisibleRows - 1);
15
+ const clampTab = useCallback((idx) => ((idx % helpTabsCount) + helpTabsCount) % helpTabsCount, [helpTabsCount]);
16
+ const open = useCallback(() => {
17
+ setHelpScrollOffset(0);
18
+ setHelpTabIndex(0);
19
+ setHelpOpen(true);
20
+ }, []);
21
+ const openAtTab = useCallback((idx) => {
22
+ setHelpScrollOffset(0);
23
+ setHelpTabIndex(clampTab(idx));
24
+ setHelpOpen(true);
25
+ }, [clampTab]);
26
+ const close = useCallback(() => setHelpOpen(false), []);
27
+ const scrollUp = useCallback(() => setHelpScrollOffset((prev) => Math.max(0, prev - 1)), []);
28
+ const scrollDown = useCallback(() => {
29
+ const maxOffset = Math.max(0, (helpTabs[helpTabIndex]?.items.length ?? 0) - helpVisibleRows);
30
+ setHelpScrollOffset((prev) => Math.min(maxOffset, prev + 1));
31
+ }, [helpTabs, helpTabIndex, helpVisibleRows]);
32
+ const goToPrevTab = useCallback(() => {
33
+ setHelpScrollOffset(0);
34
+ setHelpTabIndex((prev) => clampTab(prev - 1));
35
+ }, [clampTab]);
36
+ const goToNextTab = useCallback(() => {
37
+ setHelpScrollOffset(0);
38
+ setHelpTabIndex((prev) => clampTab(prev + 1));
39
+ }, [clampTab]);
40
+ /** Returns true if a numeric tab was selected */
41
+ const goToTab = useCallback((input) => {
42
+ if (input.length !== 1)
43
+ return false;
44
+ const num = Number.parseInt(input, 10);
45
+ if (Number.isNaN(num) || num < 1 || num > helpTabsCount)
46
+ return false;
47
+ setHelpScrollOffset(0);
48
+ setHelpTabIndex(num - 1);
49
+ return true;
50
+ }, [helpTabsCount]);
51
+ return {
52
+ helpOpen,
53
+ helpTabIndex,
54
+ helpScrollOffset,
55
+ helpVisibleRows,
56
+ open,
57
+ openAtTab,
58
+ close,
59
+ scrollUp,
60
+ scrollDown,
61
+ goToPrevTab,
62
+ goToNextTab,
63
+ goToTab,
64
+ };
65
+ }
@@ -0,0 +1,39 @@
1
+ import { useCallback } from "react";
2
+ import { useAtom } from "jotai";
3
+ import { hierarchyStateAtom } from "../state/atoms.js";
4
+ export function useHierarchyState() {
5
+ const [state, setState] = useAtom(hierarchyStateAtom);
6
+ const reset = useCallback(() => {
7
+ setState({ filters: [""], indices: [0] });
8
+ }, [setState]);
9
+ const updateCurrentFilter = useCallback((value) => {
10
+ setState((prev) => {
11
+ const nextFilters = prev.filters.length === 0 ? [value] : [...prev.filters.slice(0, -1), value];
12
+ return { ...prev, filters: nextFilters };
13
+ });
14
+ }, [setState]);
15
+ const pushLevel = useCallback((selectedIndex, nextFilter = "") => {
16
+ setState((prev) => ({
17
+ filters: [...prev.filters, nextFilter],
18
+ indices: [...prev.indices, selectedIndex],
19
+ }));
20
+ }, [setState]);
21
+ const popLevel = useCallback(() => {
22
+ const nextFilters = state.filters.length > 1 ? state.filters.slice(0, -1) : state.filters;
23
+ const nextIndices = state.indices.length > 1 ? state.indices.slice(0, -1) : state.indices;
24
+ const restoredIndex = state.indices.length > 1
25
+ ? (state.indices[state.indices.length - 1] ?? 0)
26
+ : (state.indices[0] ?? 0);
27
+ const restoredFilter = nextFilters[nextFilters.length - 1] ?? "";
28
+ setState({ filters: nextFilters, indices: nextIndices });
29
+ return { restoredFilter, restoredIndex };
30
+ }, [setState, state.filters, state.indices]);
31
+ return {
32
+ filters: state.filters,
33
+ indices: state.indices,
34
+ reset,
35
+ updateCurrentFilter,
36
+ pushLevel,
37
+ popLevel,
38
+ };
39
+ }